{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "33c0bc6b-0506-4e17-9823-07a644d3093d",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Metodi del kernel quantistico\"\n",
        "description: \"I kernel quantistici vengono utilizzati inizialmente per determinare un elemento della matrice del kernel; viene presentata una matrice completa del kernel e l'interfaccia con gli strumenti classici del kernel.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore workstreams workstream xvals yvals xval Arunachalam scikit */}\n",
        "\n",
        "<span id=\"quantum-kernels\" />\n",
        "\n",
        "# Nuclei quantistici\n",
        "\n",
        "<span id=\"introduction-to-quantum-kernels\" />\n",
        "\n",
        "## Introduzione ai kernel quantistici\n",
        "\n",
        "Il \"metodo del kernel quantistico\" si riferisce a qualsiasi metodo che utilizza computer quantistici per stimare un kernel. In questo contesto, il termine \"kernel\" si riferisce alla matrice kernel o alle singole voci in essa contenute. Ricordiamo che una mappatura delle caratteristiche $\\Phi(\\vec{x})$ è una mappatura da $\\vec{x}\\in \\mathbb{R}^d$ a $\\Phi(\\vec{x})\\in \\mathbb{R}^{d'},$, dove di solito $d'>d$ e dove l'obiettivo di questa mappatura è rendere le categorie di dati separabili da un iperpiano. La funzione kernel prende come argomenti i vettori nello spazio delle mappe di caratteristiche e restituisce il loro prodotto interno, cioè $K:\\mathbb{R}^d\\times\\mathbb{R}^d\\rightarrow \\mathbb{R}$ con $K(x,y) = \\langle \\Phi(x)|\\Phi(y)\\rangle$. Classicamente, siamo interessati a mappe di caratteristiche per le quali la funzione kernel è facile da valutare. Ciò significa spesso trovare una funzione kernel per la quale il prodotto interno nello spazio delle caratteristiche mappate può essere scritto in termini di vettori di dati originali, senza dover mai costruire $\\Phi(x)$ e $\\Phi(y)$. Nel metodo dei kernel quantistici, la mappatura delle caratteristiche viene effettuata da un circuito quantistico e il kernel viene stimato utilizzando le misure su tale circuito e le relative probabilità di misura.\n",
        "\n",
        "In questa lezione esamineremo le profondità dei circuiti di codifica precodificati che utilizzano un entanglement sostanziale e le confronteremo con le profondità dei circuiti che codifichiamo a mano. Non si tratta di sostenere un metodo piuttosto che un altro. Potreste scoprire che i circuiti precodificati sono troppo profondi e che l'entanglement nel circuito personalizzato è insufficiente per essere utile. Anche in questo caso, le indicazioni sono fornite solo per consentire l'esplorazione.\n",
        "\n",
        "Prima di illustrare in dettaglio la stima di una matrice kernel, illustriamo il flusso di lavoro utilizzando il linguaggio dei modelli Qiskit.\n",
        "\n",
        "<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",
        "* Input: Set di dati di addestramento\n",
        "* Uscita: Circuito astratto per il calcolo di una voce della matrice kernel\n",
        "\n",
        "Dato il set di dati, il punto di partenza è la codifica dei dati in un circuito quantistico. In altre parole, dobbiamo mappare i nostri dati nello spazio di Hilbert degli stati del nostro computer quantistico. Per farlo, costruiamo un circuito dipendente dai dati. Ci sono molti modi per farlo e la lezione precedente ha illustrato una serie di opzioni. È possibile costruire il proprio circuito per codificare i dati, oppure utilizzare una mappa di caratteristiche preconfezionata come `zz_feature_map`. In questa lezione faremo entrambe le cose.\n",
        "\n",
        "Si noti che per calcolare un singolo elemento della matrice kernel, si desidera codificare due punti diversi, in modo da poter stimare il loro prodotto interno. Un flusso di lavoro completo del kernel quantistico coinvolgerà ovviamente molti prodotti interni di questo tipo tra vettori di dati mappati, oltre ai metodi classici di apprendimento automatico. Ma il passo centrale che viene iterato è la stima di un singolo elemento della matrice kernel. A tal fine, selezioniamo un circuito quantistico dipendente dai dati e mappiamo due vettori di dati nello spazio delle caratteristiche.\n",
        "\n",
        "![Circuito classico](https://quantum.cloud.ibm.com/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/classical-review-background-kernel-circuit.avif)\n",
        "\n",
        "Per il compito di generare una matrice kernel, siamo particolarmente interessati alla probabilità di misurare lo stato $|0\\rangle^{\\otimes N}$, in cui tutti i qubit $N$ sono nello stato $|0\\rangle$. Per rendersene conto, si consideri che il circuito responsabile della codifica e della mappatura di un vettore di dati $\\vec{x}_i$ può essere scritto come $\\Phi(\\vec{x}_i)$, e quello responsabile della codifica e della mappatura di $\\vec{x}_j$ è $\\Phi(\\vec{x}_j)$, e si denotino gli stati mappati\n",
        "\n",
        "$$\n",
        "|\\psi(\\vec{x}_i)\\rangle = \\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}\n",
        "$$\n",
        "\n",
        "$$\n",
        "|\\psi(\\vec{x}_j)\\rangle = \\Phi(\\vec{x}_j)|0\\rangle^{\\otimes N}.\n",
        "$$\n",
        "\n",
        "Questi stati *sono* la mappatura dei dati in dimensioni superiori, quindi la voce desiderata del kernel è il prodotto interno\n",
        "\n",
        "$$\n",
        "\\langle\\psi(\\vec{x}_j)|\\psi(\\vec{x}_i)\\rangle = \\langle 0 |^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}.\n",
        "$$\n",
        "\n",
        "Se operiamo sullo stato iniziale predefinito $|0\\rangle^{\\otimes N}$ con entrambi i circuiti $\\Phi^\\dagger(\\vec{x}_j)$ e $\\Phi(\\vec{x}_i)$, la probabilità di misurare poi lo stato $|0\\rangle^{\\otimes N}$ è\n",
        "\n",
        "$$\n",
        "P_0 = |\\langle0|^{\\otimes N}\\Phi^\\dagger(\\vec{x}_j)\\Phi(\\vec{x}_i)|0\\rangle^{\\otimes N}|^2.\n",
        "$$\n",
        "\n",
        "Questo è esattamente il valore che vogliamo (fino a $||^2$ ). Il livello di misurazione del nostro circuito restituirà le probabilità di misurazione (o le cosiddette \"quasi-probabilità\", se si utilizzano determinati metodi di attenuazione degli errori). La probabilità di interesse è quella dello stato zero, $|0\\rangle^{\\otimes N}$.\n",
        "\n",
        "<span id=\"step-2-optimize-problem-for-quantum-execution\" />\n",
        "\n",
        "### Fase 2: Ottimizzazione del problema per l'esecuzione quantistica\n",
        "\n",
        "* Ingresso: Circuito astratto, non ottimizzato per un particolare backend\n",
        "* Uscita: Circuito di destinazione e osservabile, ottimizzato per la QPU selezionata\n",
        "\n",
        "In questa fase, utilizzeremo la funzione `generate_preset_pass_manager` di Qiskit per specificare una routine di ottimizzazione del nostro circuito rispetto al computer quantistico reale su cui intendiamo eseguire l'esperimento. Impostiamo `optimization_level=3` , il che significa che useremo il gestore dei passaggi preimpostati che fornisce il massimo livello di ottimizzazione. In questo contesto, \"ottimizzazione\" si riferisce all'ottimizzazione dell'implementazione del circuito su un computer quantistico reale. Ciò include considerazioni come la selezione di qubit fisici che corrispondono a qubit del circuito quantistico astratto per ridurre al minimo la profondità del gate, o la selezione di qubit fisici con i tassi di errore più bassi disponibili. Questo non è direttamente collegato all'ottimizzazione del problema di apprendimento automatico (come negli ottimizzatori classici come COBYLA).\n",
        "\n",
        "A seconda di come si implementa il passaggio 2, potrebbe essere necessario ottimizzare il circuito più di una volta, poiché ogni coppia di punti coinvolti in un elemento della matrice produce un circuito diverso da misurare.\n",
        "\n",
        "<span id=\"step-3-execute-using-ibm-quantum-primitives\" />\n",
        "\n",
        "### Fase 3: Esecuzione tramite le primitive di \" IBM Quantum \"\n",
        "\n",
        "* Ingresso: Circuito target\n",
        "* Uscita: Distribuzione di probabilità\n",
        "\n",
        "Utilizzare la primitiva `Sampler` `IBM Quantum` per ricostruire una distribuzione di probabilità degli stati ottenuta dal campionamento del circuito. Si noti che questo concetto può essere definito anche “distribuzione di quasi-probabilità”, un termine applicabile nei casi in cui il rumore costituisca un problema e quando vengono introdotti passaggi aggiuntivi, come ad esempio nella mitigazione degli errori. In tali casi, la somma di tutte le probabilità potrebbe non essere esattamente pari a 1; da qui il termine \"quasi-probabilità\".\n",
        "\n",
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Fase 4: Post-elaborazione, restituzione dei risultati in formato classico\n",
        "\n",
        "* Ingresso: Distribuzione di probabilità\n",
        "* Uscita: Un singolo elemento della matrice kernel, o una matrice kernel se si ripete\n",
        "\n",
        "Calcolare la probabilità di misurare $|0\\rangle^{\\otimes N}$ sul circuito quantistico e popolare la matrice kernel nella posizione corrispondente ai due vettori di dati utilizzati. Per riempire l'intera matrice del kernel, dobbiamo eseguire un esperimento quantistico per ogni voce. Una volta ottenuta una matrice kernel, possiamo utilizzarla in molti algoritmi classici di apprendimento automatico che accettano `pre-calculated kernels`. Ad esempio: `qml_svc = SVC(kernel=\"precomputed\")`. Possiamo quindi utilizzare i flussi di lavoro classici per applicare il nostro modello ai dati di test e ottenere un punteggio di accuratezza. In base alla soddisfazione del punteggio di accuratezza, potrebbe essere necessario rivedere alcuni aspetti del calcolo, come la mappa delle caratteristiche.\n",
        "\n",
        "<span id=\"lesson-outline\" />\n",
        "\n",
        "### Schema della lezione\n",
        "\n",
        "In questa lezione eseguiremo questi passaggi in diversi modi per sfruttare al meglio il vostro tempo sui computer quantistici reali. Applicheremo il metodo del kernel quantistico a\n",
        "\n",
        "* Una singola matrice kernel per dati con relativamente poche caratteristiche, utilizzando un backend reale, in modo da poter seguire facilmente ciò che accade in ogni fase.\n",
        "* Un intero set di dati con un numero relativamente basso di caratteristiche, utilizzando un backend simulato, in modo da poter vedere come il flusso di lavoro quantistico si collega ai metodi classici di apprendimento automatico\n",
        "* Una singola matrice kernel per dati con molte caratteristiche, utilizzando un vero computer quantistico. Non stimeremo un'intera matrice kernel per un grande insieme di dati, per rispettare i tempi dei computer quantistici IBM®.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "2fedaf0c-efb4-4425-8e31-89ca0319375d",
      "metadata": {},
      "outputs": [],
      "source": [
        "# If you have not already, install scikit learn\n",
        "#!pip install scikit-learn"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f05e564c-ae91-4365-88fb-a57183c525aa",
      "metadata": {},
      "source": [
        "<span id=\"single-kernel-matrix-entry\" />\n",
        "\n",
        "## Voce singola della matrice del kernel\n",
        "\n",
        "<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",
        "Consideriamo per prima cosa un insieme di dati con poche caratteristiche, ad esempio 10. L'insieme dei dati può essere grande quanto si vuole, poiché gli elementi della matrice kernel vengono calcolati uno alla volta. Abbiamo bisogno di almeno due punti, quindi inizieremo con questo (nel prossimo esempio, importeremo un set di dati completo). Importiamo alcuni pacchetti necessari:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "61f15d16-993a-4405-9454-685829cb08b4",
      "metadata": {},
      "outputs": [],
      "source": [
        "import pandas as pd\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "# Two mock data points, including category labels, as in training\n",
        "small_data = [\n",
        "    [-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],\n",
        "    [-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],\n",
        "]\n",
        "\n",
        "# Data points with labels removed, for inner product\n",
        "train_data = [small_data[0][:-1], small_data[1][:-1]]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e9acb5ee-8c7a-467d-a7e9-e6219f4d443b",
      "metadata": {},
      "source": [
        "Possiamo provare a utilizzare il sito `z_feature_map`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "9d8e424e-751a-4057-a879-cc39d83af953",
      "metadata": {},
      "outputs": [],
      "source": [
        "# from qiskit.circuit.library import zz_feature_map\n",
        "# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)\n",
        "\n",
        "from qiskit.circuit.library import z_feature_map\n",
        "\n",
        "fm = z_feature_map(feature_dimension=np.shape(train_data)[1])\n",
        "\n",
        "\n",
        "unitary1 = fm.assign_parameters(train_data[0])\n",
        "unitary2 = fm.assign_parameters(train_data[1])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2b41da49-ce41-45eb-83ff-cfcbaa8c2c92",
      "metadata": {},
      "source": [
        "I due unitari di cui sopra corrispondono esattamente a $U_1$ e $U_2$ descritti nell'introduzione. Possiamo combinarli usando `unitary_overlap`. Come sempre, vogliamo tenere d'occhio la profondità del nostro circuito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "4273dcf7-784c-4edb-a9cd-9b249d24dbb8",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  9\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/extracted-outputs/4273dcf7-784c-4edb-a9cd-9b249d24dbb8-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from qiskit.circuit.library import unitary_overlap\n",
        "\n",
        "\n",
        "overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "overlap_circ.measure_all()\n",
        "\n",
        "print(\"circuit depth = \", overlap_circ.decompose().depth())\n",
        "overlap_circ.decompose().draw(\"mpl\", scale=0.6, style=\"iqp\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0a9f9df0-d543-496a-99fd-8b66a53941a7",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-execution\" />\n",
        "\n",
        "### Fase 2: Ottimizzazione del problema per l'esecuzione quantistica\n",
        "\n",
        "Si inizia selezionando il backend meno trafficato, quindi si ottimizza il circuito per l'esecuzione su tale backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "27f62452-2534-4f97-a738-52e4a19a7772",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_brisbane')>\n"
          ]
        }
      ],
      "source": [
        "# Import needed packages\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "\n",
        "# Get the least busy backend\n",
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=fm.num_qubits\n",
        ")\n",
        "print(backend)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "36cfd4e9-55c9-46c4-8afe-bd6cd0a40f45",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Apply level 3 optimization to our overlap circuit\n",
        "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n",
        "overlap_ibm = pm.run(overlap_circ)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "13def807-7e1a-44c8-a7d8-2db5ce0f75ec",
      "metadata": {},
      "source": [
        "Per i circuiti più complicati, questo passaggio aumenterà sostanzialmente la profondità del circuito, poiché si tratta di una mappa delle porte native dei computer quantistici reali, e potrebbe essere necessario spostare le informazioni da un qubit all'altro. In questo semplice caso, la profondità non viene praticamente influenzata.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "79348ff5-4d6d-46d7-ae2b-e6ffbb22dd25",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  10\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "1"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "print(\"circuit depth = \", overlap_ibm.decompose().depth())\n",
        "overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ec97b263-4037-4ed9-9588-110335d2c51d",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-ibm-quantum-primitives\" />\n",
        "\n",
        "### Fase 3: Esecuzione tramite le primitive di \" IBM Quantum \"\n",
        "\n",
        "La sintassi per l'esecuzione su un simulatore è commentata di seguito. Per questo set di dati, con un numero ridotto di caratteristiche, l'esecuzione su un simulatore è ancora un'opzione. Per i calcoli su scala industriale, la simulazione non è tipicamente fattibile. I simulatori dovrebbero essere usati solo per il debug del codice ridotto.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "36a81e65-d14f-4dc2-b64c-4acbd5a185cd",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Run this for a simulator\n",
        "\n",
        "# from qiskit.primitives import StatevectorSampler\n",
        "# from qiskit_ibm_runtime import Options, Session, Sampler\n",
        "# num_shots = 10000\n",
        "\n",
        "# Evaluate the problem using state vector-based primitives from Qiskit\n",
        "\n",
        "# sampler = StatevectorSampler()\n",
        "# results = sampler.run([overlap_circ], shots=num_shots).result()\n",
        "\n",
        "# .get_counts() returns counts associated with a state labeled by bit results\n",
        "# such as |001101...01>.\n",
        "\n",
        "# counts_bit = results[0].data.meas.get_counts()\n",
        "\n",
        "# .get_int_counts returns the same counts, but labeled by integer equivalent\n",
        "# of the above bit string.\n",
        "\n",
        "# counts = results[0].data.meas.get_int_counts()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1ad869b2-ad50-4a82-bc4c-70fd0ab802c5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.\n",
        "\n",
        "# Import the IBM Quantum primitive\n",
        "from qiskit_ibm_runtime import Session, SamplerV2 as Sampler\n",
        "\n",
        "num_shots = 10000\n",
        "\n",
        "# Use sampler and get the counts\n",
        "\n",
        "sampler = Sampler(mode=backend)\n",
        "results = sampler.run([overlap_ibm], shots=num_shots).result()\n",
        "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n",
        "counts_bit = results[0].data.meas.get_counts()\n",
        "# .get_int_counts returns the same counts, but labeled by integer equivalent\n",
        "# of the above bit string.\n",
        "counts = results[0].data.meas.get_int_counts()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16d66085-047a-4718-80fe-eb64c20bd398",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Fase 4: Post-elaborazione, restituzione dei risultati in formato classico\n",
        "\n",
        "Come descritto nell'introduzione, la misura più utile è la probabilità di misurare lo stato zero $|00000\\rangle$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 62,
      "id": "33522847-21dc-48b8-9270-ce11fd1ec529",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "0.6525"
            ]
          },
          "execution_count": 62,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "counts.get(0, 0.0) / num_shots"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "65dfa3e4-bfe4-45d7-815f-e8c1aa24d916",
      "metadata": {},
      "source": [
        "Questo è il risultato che volevamo: una stima del prodotto interno (fino al mod. quadrato) dei vettori corrispondenti a due punti dati. Se vogliamo esaminare l'intera distribuzione delle probabilità di misura (o quasiprobabilità), possiamo farlo utilizzando la funzione `plot_distribution` , come mostrato di seguito. Si vede che per un numero elevato di qubit, immagini come questa diventano rapidamente intrattabili.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 30,
      "id": "29aaf5d6-ea7e-4373-b33c-9299699a3964",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/extracted-outputs/29aaf5d6-ea7e-4373-b33c-9299699a3964-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 30,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from qiskit.visualization import plot_distribution\n",
        "\n",
        "plot_distribution(counts_bit)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d0a52344-34f2-498b-8f46-489d581bd8ba",
      "metadata": {},
      "source": [
        "In alternativa, si potrebbe definire una visualizzazione come quella che segue per esaminare solo le 10 misure più probabili. Questo potrebbe essere importante per la risoluzione dei problemi o per cercare di ottenere una maggiore intuizione dei dati. Ma la probabilità di misura dello stato zero è l'elemento della nostra matrice kernel.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 29,
      "id": "c141e800-79eb-400a-9b8c-75e2393ce24e",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/extracted-outputs/c141e800-79eb-400a-9b8c-75e2393ce24e-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "def visualize_counts(probs, num_qubits):\n",
        "    \"\"\"Visualize the outputs from the Qiskit Sampler primitive.\"\"\"\n",
        "    zero_prob = probs.get(0, 0.0)\n",
        "    top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])\n",
        "    top_10.update({0: zero_prob})\n",
        "    by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))\n",
        "    xvals, yvals = list(zip(*by_key.items()))\n",
        "    xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]\n",
        "    plt.bar(xvals, yvals)\n",
        "    plt.xticks(rotation=75)\n",
        "    plt.title(\"Results of sampling\")\n",
        "    plt.xlabel(\"Measured bitstring\")\n",
        "    plt.ylabel(\"Counts\")\n",
        "    plt.show()\n",
        "\n",
        "\n",
        "visualize_counts(counts, overlap_circ.num_qubits)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "47e85d94-ee94-4ed9-8223-ccd5fc9442d4",
      "metadata": {},
      "source": [
        "Da queste informazioni su un solo prodotto interno tra due punti di dati nello spazio delle caratteristiche a più alta dimensionalità, tutto ciò che possiamo dire è che la loro sovrapposizione è abbastanza grande rispetto alla sovrapposizione massima (che sarebbe 1.0 ). Questo potrebbe essere un indicatore del fatto che questi due punti di dati sono in qualche modo simili per natura e saranno classificati nelle stesse classi. Oppure potrebbe essere un indicatore del fatto che la nostra mappa di caratteristiche non è efficace nel mappare in uno spazio in cui i dati simili hanno una forte sovrapposizione e i dati diversi hanno una piccola sovrapposizione. Per sapere quale sia la verità, dobbiamo applicare la nostra mappa di caratteristiche all'intero set di dati e vedere se la matrice kernel risultante può essere manipolata per separare efficacemente le classi con un'elevata precisione.\n",
        "\n",
        "Vale la pena di notare che abbiamo utilizzato il sito `z_feature_map` che ha dato luogo a una bassa profondità di trasposizione a due qubit (profondità 1, in effetti). Se i circuiti diventano troppo profondi, sicuramente si produrrà molto rumore e la probabilità di misurare lo stato zero sarà molto bassa, anche se la mappa delle caratteristiche è ben adattata ai dati. Ad esempio, ripetendo il procedimento sopra descritto con `zz_feature_map` e `, entanglement='linear', reps=1` si ottiene `dist.get(0,0.0) = 0.0015` utilizzando gli stessi punti dati. Ciò è dovuto alle profondità dei circuiti e dei due-qubit molto più elevate di `zz_feature_map`. La figura seguente mostra la distribuzione di probabilità per questo calcolo.\n",
        "\n",
        "![Risultati negativi da una mappa di caratteristiche zz.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-machine-learning/quantum-kernel-methods/zzfeaturemap-bad-results.avif)\n",
        "\n",
        "Vale la pena di giocare con alcuni punti dati della stessa categoria per vedere quanto deve essere bassa la profondità per ottenere buoni risultati. Quelli che seguono sono consigli di massima che sicuramente avranno delle eccezioni. In generale, una profondità di 10 o meno di 10 transpiled a due qubit non dovrebbe costituire un problema. Una profondità di 50-60 transpiled a due qubit è lo stato dell'arte e richiederà, tra gli altri strumenti, una mitigazione avanzata degli errori. Nel mezzo, i risultati possono variare in base alla somiglianza dei dati, all'espressività della mappa delle caratteristiche, alla larghezza del circuito e ad altri fattori.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "33eda2a4-cd97-4bba-8acd-4017bfe2cd12",
      "metadata": {},
      "source": [
        "Normalmente la fase di post-elaborazione comprende anche i classici processi di apprendimento automatico. Nella prossima sezione estenderemo questo processo a un intero set di dati e mostreremo il classico flusso di lavoro dell'apprendimento automatico.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "41850d1d-3800-4aa0-aa01-fe6e72c872be",
      "metadata": {},
      "source": [
        "<span id=\"check-your-understanding\" />\n",
        "\n",
        "#### Verifica la tua comprensione\n",
        "\n",
        "In un circuito quantistico a 10 qubit, in generale, quanti stati diversi si possono misurare?\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Risposta\">\n",
        "    $2^{10}$ o 1024.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n",
        "Supponiamo che una persona alle prime armi con l'informatica quantistica tenti di utilizzare un circuito quantistico con una profondità a due qubit molto elevata, senza ricorrere alla mitigazione degli errori. Supponiamo inoltre che ciò comporti un tasso di errore del 10% su ciascun qubit. Se l'elemento della matrice del kernel vero (privo di errori) corrispondente a questo circuito fosse molto grande, ad esempio 1.0, quale sarebbe la probabilità di misurare tutti e 10 i qubit nello stato in cui ogni qubit è $|0\\rangle$?\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Risposta\">\n",
        "    La probabilità che ogni qubit si trovi correttamente nello stato |0> è 0.90. La probabilità che tutti i 10 qubit si trovino nello stato corretto è $0.90^{10}$ o circa il 35%.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n",
        "Spiegate con parole vostre perché è così importante monitorare le profondità dei circuiti. Questo è vero in generale, ma spiegatelo nel contesto della stima del kernel quantistico.\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Risposta\">\n",
        "    In questo flusso di lavoro QKE, le nostre stime si basano sulle misure dello stato zero, ovvero lo stato in cui ogni qubit si trova nello stato $|0\\rangle$. I circuiti molto profondi introdurranno alti tassi di errore. Se questo tasso di errore viene sommato a quello di molti qubit, la probabilità di misurare lo stato zero si riduce in modo sostanziale.\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0fde5aca-0d54-40b1-82d4-9d1b2a1b67bb",
      "metadata": {},
      "source": [
        "<span id=\"full-kernel-matrix\" />\n",
        "\n",
        "## Matrice kernel completa\n",
        "\n",
        "In questa sezione, estenderemo il processo sopra descritto alla classificazione binaria di un set di dati completo. In questo modo si introdurranno due componenti importanti: (1) possiamo ora implementare l'apprendimento automatico classico in post-elaborazione e (2) possiamo ottenere punteggi di accuratezza per la nostra formazione.\n",
        "\n",
        "<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",
        "Ora importeremo un set di dati esistente per la nostra classificazione. Questo set di dati è composto da 128 righe (punti dati) e 14 caratteristiche per ogni punto. C'è un 15° elemento che indica la categoria binaria di ogni punto ( $\\pm 1$ ). Il set di dati è importato di seguito, oppure è possibile accedere al set di dati e visualizzarne la struttura [qui](https://github.com/qiskit-community/prototype-quantum-kernel-training/blob/main/data/dataset_graph7.csv).\n",
        "\n",
        "Utilizzeremo i primi 90 punti di dati per l'addestramento e i successivi 30 punti per i test.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "60f2699d-ef47-44f7-931a-4a70d4363c0e",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "--2024-07-11 23:05:22--  https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n",
            "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...\n",
            "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n",
            "HTTP request sent, awaiting response... 200 OK\n",
            "Length: 49405 (48K) [text/plain]\n",
            "Saving to: ‘dataset_graph7.csv.15’\n",
            "\n",
            "dataset_graph7.csv. 100%[===================>]  48.25K  --.-KB/s    in 0.02s   \n",
            "\n",
            "2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]\n",
            "\n"
          ]
        }
      ],
      "source": [
        "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n",
        "\n",
        "df = pd.read_csv(\"dataset_graph7.csv\", sep=\",\", header=None)\n",
        "\n",
        "# Prepare training data\n",
        "\n",
        "train_size = 90\n",
        "X_train = df.values[0:train_size, :-1]\n",
        "train_labels = df.values[0:train_size, -1]\n",
        "\n",
        "# Prepare testing data\n",
        "test_size = 30\n",
        "X_test = df.values[train_size : train_size + test_size, :-1]\n",
        "test_labels = df.values[train_size : train_size + test_size, -1]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4a665fcb-bd27-4713-9c0a-b7747de5fb45",
      "metadata": {},
      "source": [
        "Ci prepariamo già a memorizzare uscite multiple costruendo una matrice kernel e una matrice test di dimensioni appropriate.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "34d92041-0501-4c20-b780-ae5352510637",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Empty kernel matrix\n",
        "num_samples = np.shape(X_train)[0]\n",
        "kernel_matrix = np.full((num_samples, num_samples), np.nan)\n",
        "test_matrix = np.full((test_size, num_samples), np.nan)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "920233dd-cfac-498d-be88-746719083cc2",
      "metadata": {},
      "source": [
        "Ora creiamo una mappa di caratteristiche per codificare e mappare i nostri dati classici in un circuito quantistico. Siamo liberi di costruire la nostra mappa delle caratteristiche o di utilizzarne una già pronta. Sentitevi liberi di modificare la mappa delle caratteristiche qui sotto o di tornare a ZFeatureMap. Ma prestate sempre attenzione alla profondità del circuito. Ricordiamo che nel precedente esempio a 6-qubit la profondità del circuito transpilato era intrattabile quando si utilizzava `zz_feature_map`. Con l'aumento della scala e della complessità del circuito, la profondità potrebbe aumentare rapidamente fino al punto in cui il rumore sovrasta i nostri risultati. Quando si conosce qualcosa della struttura dei dati che può indicare la struttura della mappa delle caratteristiche più utile, è consigliabile creare una mappa delle caratteristiche personalizzata che sfrutti tale conoscenza.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "ca813500-5856-4b50-a213-a7e87d825c18",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n",
        "\n",
        "# Prepare feature map for computing overlap\n",
        "num_features = np.shape(X_train)[1]\n",
        "num_qubits = int(num_features / 2)\n",
        "\n",
        "# To use a custom feature map use the lines below.\n",
        "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n",
        "\n",
        "fm = QuantumCircuit(num_qubits)\n",
        "training_param = Parameter(\"θ\")\n",
        "feature_params = ParameterVector(\"x\", num_qubits * 2)\n",
        "fm.ry(training_param, fm.qubits)\n",
        "for cz in entangler_map:\n",
        "    fm.cz(cz[0], cz[1])\n",
        "for i in range(num_qubits):\n",
        "    fm.rz(-2 * feature_params[2 * i + 1], i)\n",
        "    fm.rx(-2 * feature_params[2 * i], i)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3c6416b5-9eb1-40a4-b72c-8912df931139",
      "metadata": {},
      "source": [
        "<span id=\"steps-2-and-3-optimize-problem-and-execute-using-primitives\" />\n",
        "\n",
        "### Passaggi 2 e 3: Ottimizzare il problema ed eseguire utilizzando primitive\n",
        "\n",
        "Costruiremo un circuito di sovrapposizione e, se in questo esempio stessimo lavorando su un vero computer quantistico, lo ottimizzeremmo per l'esecuzione come prima. In questo caso, però, intendiamo passare su tutti i punti dati e calcolare l'intera matrice kernel. Per ogni coppia di vettori di dati $\\vec{x}_i$ e $\\vec{x}_j$, creiamo un diverso circuito di sovrapposizione. Dobbiamo quindi ottimizzare il nostro circuito per ogni coppia di punti dati. Quindi le fasi 2 e 3 verrebbero eseguite insieme nelle iterazioni multiple.\n",
        "\n",
        "La cella di codice sottostante esegue esattamente lo stesso processo di prima per una coppia di punti dati singoli. Questa volta viene eseguito semplicemente all'interno di due loop `for` e c'è una riga aggiuntiva alla fine `kernel_matrix[x_1,x_2] = ...` per memorizzare i risultati di ogni calcolo. Si noti che abbiamo sfruttato la simmetria di una matrice kernel per ridurre di 1/2 il numero di calcoli. Abbiamo anche impostato semplicemente gli elementi diagonali a 1, come dovrebbero essere in assenza di rumore. A seconda dell'implementazione e della precisione richiesta, è possibile utilizzare gli elementi diagonali per stimare il rumore o per conoscerlo ai fini della mitigazione degli errori.\n",
        "\n",
        "Una volta che la matrice del kernel è stata completamente popolata, si ripete il processo per i dati di prova e si popola la matrice di prova. Anche questa è in realtà una matrice kernel; le diamo semplicemente un nome diverso per distinguerle.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7995748f-fc97-4eda-bed6-8b189c1e65ea",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "training done\n",
            "test matrix done\n"
          ]
        }
      ],
      "source": [
        "# To use a simulator\n",
        "from qiskit.primitives import StatevectorSampler\n",
        "\n",
        "# Remember to insert your token in the QiskitRuntimeService constructor\n",
        "# to use real quantum computers\n",
        "\n",
        "# service = QiskitRuntimeService()\n",
        "# backend = service.least_busy(\n",
        "#    operational=True, simulator=False, min_num_qubits=fm.num_qubits\n",
        "# )\n",
        "\n",
        "num_shots = 10000\n",
        "\n",
        "# Evaluate the problem using state vector-based primitives from Qiskit.\n",
        "sampler = StatevectorSampler()\n",
        "\n",
        "for x1 in range(0, train_size):\n",
        "    for x2 in range(x1 + 1, train_size):\n",
        "        unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])\n",
        "        unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n",
        "\n",
        "        # Create the overlap circuit\n",
        "        overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "        overlap_circ.measure_all()\n",
        "\n",
        "        # These lines run the qiskit sampler primitive.\n",
        "        counts = (\n",
        "            sampler.run([overlap_circ], shots=num_shots)\n",
        "            .result()[0]\n",
        "            .data.meas.get_int_counts()\n",
        "        )\n",
        "\n",
        "        # Assign the probability of the 0 state to the kernel matrix, and the transposed element\n",
        "        # (since this is an inner product)\n",
        "        kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n",
        "        kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots\n",
        "    # Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to\n",
        "    # probability (or alter the code to check these entries and verify they yield 1)\n",
        "    kernel_matrix[x1, x1] = 1\n",
        "\n",
        "print(\"training done\")\n",
        "\n",
        "# Similar process to above, but for testing data.\n",
        "for x1 in range(0, test_size):\n",
        "    for x2 in range(0, train_size):\n",
        "        unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])\n",
        "        unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])\n",
        "\n",
        "        # Create the overlap circuit\n",
        "        overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "        overlap_circ.measure_all()\n",
        "\n",
        "        counts = (\n",
        "            sampler.run([overlap_circ], shots=num_shots)\n",
        "            .result()[0]\n",
        "            .data.meas.get_int_counts()\n",
        "        )\n",
        "\n",
        "        test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n",
        "\n",
        "print(\"test matrix done\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ea65cd41-4ba3-423c-a98f-2f8e3751adf4",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Fase 4: Post-elaborazione, restituzione dei risultati in formato classico\n",
        "\n",
        "Ora che disponiamo di una matrice kernel e di una matrice test formattata in modo simile dai metodi kernel quantistici, possiamo applicare gli algoritmi classici di apprendimento automatico per fare previsioni sui nostri dati di prova e verificarne l'accuratezza. Inizieremo importando `sklearn.svc`, un classificatore a vettori di supporto (SVC) di Scikit-Learn. Dobbiamo specificare che vogliamo che l'SVC usi il nostro kernel precalcolato usando `kernel = precomputed`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "78d9e291-54c3-4c45-827e-066c6d012da5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# import a support vector classifier from a classical ML package.\n",
        "from sklearn.svm import SVC\n",
        "\n",
        "# Specify that you want to use a pre-computed kernel matrix\n",
        "qml_svc = SVC(kernel=\"precomputed\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9bd69e5f-dafd-492c-bd5d-f5e16925f611",
      "metadata": {},
      "source": [
        "Utilizzando `SVC.fit`, possiamo ora inserire la matrice del kernel e le etichette di addestramento per ottenere un adattamento. `SVC.score` valuterà quindi i nostri dati di prova rispetto a tale adattamento utilizzando la nostra matrice di prova e restituirà la nostra accuratezza.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d8708973-c97c-494c-af9c-e4a0fe430452",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Precomputed kernel classification test score: 1.0\n"
          ]
        }
      ],
      "source": [
        "# Feed in the pre-computed matrix and the labels of the training data.\n",
        "# The classical algorithm gives you a fit.\n",
        "qml_svc.fit(kernel_matrix, train_labels)\n",
        "\n",
        "# Now use the .score to test your data, using the matrix of test data,\n",
        "# and test labels as your inputs.\n",
        "qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)\n",
        "print(f\"Precomputed kernel classification test score: {qml_score_precomputed_kernel}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0200dceb-6d61-4e16-8c3d-d08dbc5529e5",
      "metadata": {},
      "source": [
        "Vediamo che l'accuratezza del nostro modello addestrato è stata del 100%. È un'ottima cosa e dimostra che la QKE può funzionare. Ma questo è molto diverso dal vantaggio quantistico. I kernel classici sarebbero stati probabilmente in grado di risolvere anche questo problema di classificazione con una precisione del 100%. C'è molto lavoro da fare per caratterizzare i diversi tipi di dati e le relazioni tra i dati per capire dove i kernel quantistici saranno più utili nell'attuale era dell'utilità.\n",
        "Lasciamo all'allievo la possibilità di modificare parti di questo flusso di lavoro e di studiare l'efficacia di varie mappe di caratteristiche quantistiche. Ecco alcuni elementi da considerare:\n",
        "\n",
        "* Quanto è robusta la precisione? Vale per un'ampia tipologia di dati o solo per questi dati specifici di formazione?\n",
        "* Quale struttura dei vostri dati vi fa sospettare l'utilità di una mappa di caratteristiche quantistiche?\n",
        "* Come viene influenzata l'accuratezza aumentando/diminuendo la quantità di dati di addestramento?\n",
        "* Quali mappe caratteristiche si possono utilizzare e come variano i risultati con le mappe caratteristiche?\n",
        "* Come vengono influenzati l'accuratezza e il tempo di esecuzione dall'aumento del numero di caratteristiche?\n",
        "* Quali tendenze, se ce ne sono, prevede che si affermino nei veri computer quantistici?\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "da2427c4-a1a1-4284-b10f-95d5c5230755",
      "metadata": {},
      "source": [
        "<span id=\"scaling-to-more-features-and-qubits\" />\n",
        "\n",
        "## Scalabilità verso più funzionalità e qubit\n",
        "\n",
        "In questa sezione, ripeteremo il calcolo di un singolo elemento della matrice, ma per un numero molto maggiore di caratteristiche, abbozzando il percorso di scala verso l'utilità. La limitazione a un singolo elemento della matrice è stata fatta in modo che il processo possa essere mostrato senza consumare troppo tempo sui computer quantistici.\n",
        "\n",
        "<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",
        "Assumiamo come punto di partenza un set di dati in cui ogni punto di dati ha 42 caratteristiche. Come nel primo esempio, calcoleremo un singolo elemento della matrice kernel, richiedendo due punti dati. I due punti sottostanti hanno 42 caratteristiche e un'unica variabile di categoria ( $\\pm 1$ ).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 79,
      "id": "014ba70a-5202-4c86-b886-7ff27ae9a1b3",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Two mock data points, including category labels, as in training\n",
        "\n",
        "large_data = [\n",
        "    [\n",
        "        -0.028,\n",
        "        -1.49,\n",
        "        -1.698,\n",
        "        0.107,\n",
        "        -1.536,\n",
        "        -1.538,\n",
        "        -1.356,\n",
        "        -1.514,\n",
        "        -0.109,\n",
        "        -1.8,\n",
        "        -0.122,\n",
        "        -1.651,\n",
        "        -1.955,\n",
        "        -0.123,\n",
        "        -1.732,\n",
        "        0.091,\n",
        "        -0.048,\n",
        "        -0.128,\n",
        "        -0.026,\n",
        "        0.082,\n",
        "        -1.263,\n",
        "        0.065,\n",
        "        0.004,\n",
        "        -0.055,\n",
        "        -0.08,\n",
        "        -0.173,\n",
        "        -1.734,\n",
        "        -0.39,\n",
        "        -1.451,\n",
        "        0.078,\n",
        "        -1.578,\n",
        "        -0.025,\n",
        "        -0.184,\n",
        "        -0.119,\n",
        "        -1.336,\n",
        "        0.055,\n",
        "        -0.204,\n",
        "        -1.578,\n",
        "        0.132,\n",
        "        -0.121,\n",
        "        -1.599,\n",
        "        -0.187,\n",
        "        -1,\n",
        "    ],\n",
        "    [\n",
        "        -1.414,\n",
        "        -1.439,\n",
        "        -1.606,\n",
        "        0.246,\n",
        "        -1.673,\n",
        "        0.002,\n",
        "        -1.317,\n",
        "        -1.262,\n",
        "        -0.178,\n",
        "        -1.814,\n",
        "        0.013,\n",
        "        -1.619,\n",
        "        -1.86,\n",
        "        -0.25,\n",
        "        -0.212,\n",
        "        -0.214,\n",
        "        -0.033,\n",
        "        0.071,\n",
        "        -0.11,\n",
        "        -1.607,\n",
        "        0.441,\n",
        "        -0.143,\n",
        "        -0.009,\n",
        "        -1.655,\n",
        "        -1.579,\n",
        "        0.381,\n",
        "        -1.86,\n",
        "        -0.079,\n",
        "        -0.088,\n",
        "        -0.058,\n",
        "        -1.481,\n",
        "        -0.064,\n",
        "        -0.065,\n",
        "        -1.507,\n",
        "        0.177,\n",
        "        -0.131,\n",
        "        -0.153,\n",
        "        0.07,\n",
        "        -1.627,\n",
        "        0.593,\n",
        "        -1.547,\n",
        "        -0.16,\n",
        "        -1,\n",
        "    ],\n",
        "]\n",
        "train_data = [large_data[0][:-1], large_data[1][:-1]]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3e4dd6a7-e51c-4dc2-99ea-66e2746e411d",
      "metadata": {},
      "source": [
        "Ricordiamo che `zz_feature_map` ha prodotto circuiti piuttosto profondi nel caso di un numero relativamente basso di caratteristiche (14 caratteristiche). Man mano che aumentiamo il numero di funzioni, dobbiamo monitorare attentamente la profondità del circuito. Per illustrarlo, proviamo prima a utilizzare il sito `zz_feature_map` e a verificare la profondità del circuito risultante.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "bdb1ecc4-7827-4eb7-bf7b-5a09838775ca",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit.library import zz_feature_map\n",
        "\n",
        "fm = zz_feature_map(\n",
        "    feature_dimension=np.shape(train_data)[1], entanglement=\"linear\", reps=1\n",
        ")\n",
        "\n",
        "unitary1 = fm.assign_parameters(train_data[0])\n",
        "unitary2 = fm.assign_parameters(train_data[1])"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5bc865cb-8f8b-4812-9a05-1bd34dbcbcb9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  251\n",
            "two-qubit depth 165\n"
          ]
        }
      ],
      "source": [
        "from qiskit.circuit.library import unitary_overlap\n",
        "\n",
        "\n",
        "overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "overlap_circ.measure_all()\n",
        "\n",
        "print(\"circuit depth = \", overlap_circ.decompose(reps=2).depth())\n",
        "print(\n",
        "    \"two-qubit depth\",\n",
        "    overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),\n",
        ")\n",
        "# overlap_circ.draw(\"mpl\", scale=0.6, style=\"iqp\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b3348ab6-ef4e-40d2-b803-e86484e0b8f3",
      "metadata": {},
      "source": [
        "Come descritto in precedenza, determinare con esattezza quanto sia profonda la profondità è una questione ricca di sfumature. Ma una profondità di due qubit superiore a 100, anche prima della transpilazione, non è un'opzione valida. Questo è il motivo per cui le mappe di caratteristiche personalizzate sono state enfatizzate nel corso di questa lezione. Se si conosce la struttura dell'intero set di dati, si dovrebbe progettare una mappa di entanglement tenendo conto di tale struttura. In questo caso, dato che stiamo calcolando solo il prodotto interno tra due punti di dati, abbiamo privilegiato una bassa profondità del circuito rispetto a qualsiasi considerazione dettagliata della struttura dei dati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 95,
      "id": "784f8a68-0d77-433b-8482-bac38ddb8adc",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit\n",
        "\n",
        "# Prepare feature map for computing overlap\n",
        "\n",
        "entangler_map = [\n",
        "    [3, 4],\n",
        "    [2, 5],\n",
        "    [1, 4],\n",
        "    [2, 3],\n",
        "    [4, 6],\n",
        "    [7, 9],\n",
        "    [10, 11],\n",
        "    [9, 12],\n",
        "    [8, 11],\n",
        "    [9, 10],\n",
        "    [11, 13],\n",
        "    [14, 16],\n",
        "    [17, 18],\n",
        "    [16, 19],\n",
        "    [15, 18],\n",
        "    [16, 17],\n",
        "    [18, 20],\n",
        "]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 96,
      "id": "587daff1-7c5b-4505-8064-827c1fbdc742",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Use the entangler map above to build a feature map\n",
        "\n",
        "num_features = np.shape(train_data)[1]\n",
        "num_qubits = int(num_features / 2)\n",
        "\n",
        "fm = QuantumCircuit(num_qubits)\n",
        "training_param = Parameter(\"θ\")\n",
        "feature_params = ParameterVector(\"x\", num_qubits * 2)\n",
        "fm.ry(training_param, fm.qubits)\n",
        "for cz in entangler_map:\n",
        "    fm.cz(cz[0], cz[1])\n",
        "for i in range(num_qubits):\n",
        "    fm.rz(-2 * feature_params[2 * i + 1], i)\n",
        "    fm.rx(-2 * feature_params[2 * i], i)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "02576ae9-8f24-4f47-8c37-ae0c3f710d4c",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit.library import unitary_overlap\n",
        "\n",
        "# Assign features of each data point to a unitary, an instance of the general feature map.\n",
        "\n",
        "unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])\n",
        "unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])\n",
        "\n",
        "# Create the overlap circuit\n",
        "\n",
        "overlap_circ = unitary_overlap(unitary1, unitary2)\n",
        "overlap_circ.measure_all()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fbe9ba90-6463-488d-b6f2-e9d4ee8f0871",
      "metadata": {},
      "source": [
        "Non ci preoccuperemo ancora di verificare le profondità, poiché ciò che conta davvero è la profondità a due qubit trasposta.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9a5c14fc-aa58-461f-a1d5-fc644e042ecc",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-execution\" />\n",
        "\n",
        "### Fase 2: Ottimizzazione del problema per l'esecuzione quantistica\n",
        "\n",
        "Si inizia selezionando il backend meno trafficato, quindi si ottimizza il circuito per l'esecuzione su tale backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "99e3a8c7-9aba-4890-a492-cb3779b5de03",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_brisbane')>\n"
          ]
        }
      ],
      "source": [
        "# Import needed packages\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "\n",
        "# Get the least busy backend\n",
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=fm.num_qubits\n",
        ")\n",
        "print(backend)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0b524533-7422-4d91-a3d1-82dfdeaf1588",
      "metadata": {},
      "source": [
        "Nei lavori di piccole dimensioni, un gestore di passaggi preimpostato spesso restituisce lo stesso circuito con la stessa profondità, in modo affidabile. Tuttavia, in circuiti molto grandi e complessi, il gestore dei passaggi può restituire circuiti transpilati diversi a ogni esecuzione. Ciò è dovuto all'uso di euristiche e al fatto che i circuiti molto grandi presentano un panorama complicato di possibili ottimizzazioni. Spesso è utile trasporre alcune volte e prendere il circuito più superficiale. Questo introduce solo un overhead classico e può migliorare sostanzialmente i risultati del computer quantistico.\n",
        "\n",
        "In questo caso, transpiliamo il circuito di sovrapposizione unitario 20 volte e osserviamo le profondità dei circuiti ottenuti.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 99,
      "id": "f5f8f833-8ac2-4df0-8268-4d83f841fd16",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "circuit depth =  61\n"
          ]
        }
      ],
      "source": [
        "# Apply level 3 optimization to our overlap circuit\n",
        "transpiled_qcs = []\n",
        "transpiled_depths = []\n",
        "transpiled_twoqubit_depths = []\n",
        "for i in range(1, 20):\n",
        "    pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n",
        "    overlap_ibm = pm.run(overlap_circ)\n",
        "    transpiled_qcs.append(overlap_ibm)\n",
        "    transpiled_depths.append(overlap_ibm.decompose().depth())\n",
        "    transpiled_twoqubit_depths.append(\n",
        "        overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)\n",
        "    )\n",
        "\n",
        "print(\"circuit depth = \", overlap_ibm.decompose().depth())"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 100,
      "id": "fd604f87-32d1-42dd-a1fb-f2315cb4dfdc",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]\n",
            "[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]\n"
          ]
        }
      ],
      "source": [
        "print(transpiled_depths)\n",
        "print(transpiled_twoqubit_depths)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "30fad0d8-f5e4-4dcf-946a-956f23334e9c",
      "metadata": {},
      "source": [
        "Qui si può notare che c'è una certa variazione nella profondità totale del gate con diversi passaggi di transpilazione. Il nostro circuito non è ancora sufficientemente profondo/ampio per vedere la variazione delle profondità di trasposizione dei due qubit. Utilizzeremo il sito `transpiled_qcs[1]`, che ha una profondità di 60, di poco inferiore a quella del circuito più profondo ottenuto, che era 77.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 101,
      "id": "28f00388-0e6e-41db-a0b4-9200d58ba5cb",
      "metadata": {},
      "outputs": [],
      "source": [
        "overlap_ibm = transpiled_qcs[1]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e96cee51-e79f-4eab-829b-397645bc1080",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-ibm-quantum-primitives\" />\n",
        "\n",
        "### Fase 3: Esecuzione tramite le primitive di \" IBM Quantum \"\n",
        "\n",
        "Quando ci avviciniamo all'utilità, i simulatori non saranno più utili. Qui viene mostrata solo la sintassi dei veri computer quantistici.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 102,
      "id": "7eedcf5f-5ead-4eb0-b423-669901f572ba",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Run on ibm_osaka, 7-12-24, required 22 sec.\n",
        "\n",
        "# Import our IBM Quantum primitive\n",
        "from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
        "\n",
        "# Open a session:\n",
        "session = Session(backend=backend)\n",
        "num_shots = 10000\n",
        "# Use sampler and get the counts\n",
        "\n",
        "sampler = Sampler(mode=session)\n",
        "options = sampler.options\n",
        "options.dynamical_decoupling.enable = True\n",
        "options.twirling.enable_gates = True\n",
        "counts = (\n",
        "    sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()\n",
        ")\n",
        "\n",
        "# Close session after done\n",
        "session.close()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f40540b8-146d-4165-af12-db902232300d",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-return-result-in-classical-format\" />\n",
        "\n",
        "### Fase 4: Post-elaborazione, restituzione dei risultati in formato classico\n",
        "\n",
        "Come descritto nell'introduzione, la misura più utile è la probabilità di misurare lo stato zero $|00000\\rangle$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 103,
      "id": "ce9f4dc1-b562-437e-b9fe-5a1781b67bfe",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "0.0138"
            ]
          },
          "execution_count": 103,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "counts.get(0, 0.0) / num_shots"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9b6c78b4-6788-404f-bd90-d98d7b288d42",
      "metadata": {},
      "source": [
        "Questo processo per il singolo elemento della matrice kernel può essere ripetuto tra le altre coppie di dati del set per ottenere la matrice kernel completa. La dimensione della matrice kernel è dettata dal numero di punti nei dati di addestramento, non dal numero di caratteristiche. Quindi il costo di calcolo della manipolazione della matrice kernel in un modello predittivo non scala come il numero di caratteristiche o di qubit. Anche per insiemi di dati relativamente piccoli con un gran numero di caratteristiche, i dati devono essere abbinati a una mappa di caratteristiche che produca una classificazione efficace.\n",
        "\n",
        "<span id=\"scaling-and-future-work\" />\n",
        "\n",
        "### Scalabilità e lavoro futuro\n",
        "\n",
        "Il metodo kernel richiede che la misura di $|0\\rangle$ sia il più accurata possibile. Ma gli errori di gate e gli errori di lettura significano che esiste una probabilità non nulla $p$ che un dato qubit venga erroneamente misurato nello stato $|1\\rangle$. Anche con l'eccessiva semplificazione che la probabilità di $|0\\rangle$ dovrebbe essere $100\\%$, per molte caratteristiche codificate, ad esempio, su $N$ bit, la probabilità di misurare correttamente tutti i bit $|0\\rangle$ si riduce a $(1-p)^N$. Quando $N$ diventa grande, questo metodo diventa sempre meno affidabile. Superare questa difficoltà e scalare la stima del kernel a un numero sempre maggiore di caratteristiche è un'area di ricerca attuale. Per saperne di più su questo problema, si veda questo lavoro di [Thanasilp, Wang, Cerezo e Holmes.](https://www.nature.com/articles/s41467-024-49287-w) Vi consigliamo di esplorare ciò che è possibile fare con gli attuali computer quantistici e di guardare anche a ciò che sarà possibile fare nell'era della correzione degli errori.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "556eb119-2b22-4bd9-b82e-c3a6ea31cfa1",
      "metadata": {},
      "source": [
        "<span id=\"review\" />\n",
        "\n",
        "### Riesamina\n",
        "\n",
        "Il calcolo di un kernel quantistico comporta\n",
        "\n",
        "* calcolare le voci della matrice kernel, utilizzando coppie di punti dati di addestramento\n",
        "* codificare i dati e mapparli attraverso una mappatura delle caratteristiche\n",
        "* ottimizzare il circuito per l'esecuzione su computer quantistici reali / backend\n",
        "\n",
        "Il kernel quantistico può quindi essere utilizzato negli algoritmi classici di apprendimento automatico, come in questa lezione.\n",
        "\n",
        "Alcuni elementi chiave da tenere a mente quando si utilizzano i kernel quantistici sono:\n",
        "\n",
        "* È probabile che il set di dati tragga beneficio dai metodi quantum kernel?\n",
        "* Provare diverse mappe di caratteristiche e schemi di entanglement.\n",
        "* La profondità del circuito è accettabile?\n",
        "* Provare a eseguire più volte un pass manager e utilizzare il circuito più piccolo possibile.\n",
        "\n",
        "I metodi dei kernel quantistici sono strumenti potenzialmente potenti, se si considera un'adeguata corrispondenza tra i set di dati con caratteristiche quantistiche e un'adeguata mappa di caratteristiche quantistiche. Per capire meglio dove i kernel quantistici possono essere utili, si consiglia di leggere [Liu, Arunachalam & Temme (2021)](https://www.nature.com/articles/s41567-021-01287-z).\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"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}