{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "c77f1777-ecb8-4cb0-9bf2-49d7c989b12a",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Migliorare la classificazione delle caratteristiche utilizzando kernel quantistici proiettati\"\n",
        "description: \"Tutorial sul miglioramento della classificazione delle caratteristiche utilizzando kernel quantistici proiettati\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore braket sqrtm cytotoxicity  Nalm Cytotoxicity binarize Utro Filippo Hsin */}\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bee84331-1f3b-4a23-b691-4d3f7f51e76b",
      "metadata": {},
      "source": [
        "<span id=\"enhance-feature-classification-using-projected-quantum-kernels\" />\n",
        "\n",
        "# Migliorare la classificazione delle caratteristiche utilizzando kernel quantistici proiettati\n",
        "\n",
        "*Stima dell'utilizzo: 80 minuti con un processore Heron r3 (NOTA: questa è solo una stima. Il tempo di esecuzione potrebbe variare)*\n",
        "\n",
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Risultati di apprendimento\n",
        "\n",
        "Dopo aver seguito questo tutorial, gli utenti dovrebbero aver compreso:\n",
        "\n",
        "* Come funzionano i kernel quantistici proiettati (PQK) e in quali casi offrono un potenziale vantaggio quantistico.\n",
        "* Come eseguire un PQK su hardware utilizzando un set di dati reali.\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Prerequisiti\n",
        "\n",
        "Consigliamo agli utenti di acquisire familiarità con gli argomenti seguenti prima di seguire questo tutorial:\n",
        "\n",
        "* [Kernel quantistici](/learning/courses/quantum-machine-learning/quantum-kernel-methods) tratti dal [corso di apprendimento automatico quantistico](/learning/courses/quantum-machine-learning) disponibile all'indirizzo IBM Quantum® Learning\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## Sfondo\n",
        "\n",
        "In questo tutorial dimostriamo come eseguire un [projected quantum kernel](https://www.nature.com/articles/s41467-021-22539-9) (PQK) con Qiskit su un set di dati biologici reali, basandoci sull'articolo [Enhanced Prediction of CAR T-Cell Cytotoxicity with Quantum-Kernel Methods](https://arxiv.org/abs/2507.22710) [\\[1\\]](#references).\n",
        "\n",
        "Il PQK è un metodo utilizzato nell'apprendimento automatico quantistico (QML) per codificare i dati classici in uno spazio di caratteristiche quantistiche e proiettarli nuovamente nel dominio classico, utilizzando i computer quantistici per migliorare la selezione delle caratteristiche. Si tratta di codificare dati classici in stati quantistici utilizzando un circuito quantistico, in genere attraverso un processo chiamato feature mapping, in cui i dati vengono trasformati in uno spazio di Hilbert ad alta dimensione. L'aspetto \"proiettato\" si riferisce all'estrazione di informazioni classiche dagli stati quantistici, misurando osservabili specifici, per costruire una matrice kernel che può essere utilizzata in algoritmi classici basati su kernel come le macchine vettoriali di supporto. Questo approccio sfrutta i vantaggi computazionali dei sistemi quantistici per ottenere potenzialmente prestazioni migliori su determinati compiti rispetto ai metodi classici.\n",
        "\n",
        "Gli elementi costitutivi principali dei PQK sono le matrici di densità ridotta (RDM), ottenute tramite misurazioni proiettive della mappa delle caratteristiche quantistiche. In particolare, solitamente si calcolano le matrici di densità ridotte a un singolo qubit (1 RDM) per ciascun qubit. Queste grandezze misurate vengono poi utilizzate come input per una funzione kernel classica, come ad esempio un kernel esponenziale, al fine di costruire la matrice kernel finale.\n",
        "\n",
        "I PQK offrono potenziali vantaggi rispetto [ai kernel quantistici](/docs/tutorials/quantum-kernel-training) standard, in particolare per l'hardware quantistico di prossima generazione. I kernel quantistici standard si basano in genere sulla stima delle sovrapposizioni globali degli stati, che diventano sempre più difficili da misurare con precisione all’aumentare del numero di qubit e sono altamente sensibili al rumore. Al contrario, i PQK utilizzano osservabili locali quali le matrici di densità ridotte a singolo qubit (1 RDM), il che comporta un minore overhead di campionamento, una maggiore robustezza al rumore hardware e una migliore scalabilità. Proiettando gli stati quantistici su caratteristiche di misurazione locali prima di applicare una funzione kernel classica, i PQK sono in grado di conservare utili correlazioni quantistiche pur rimanendo più pratici per i dispositivi di prossima generazione.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "542b8075-3b8c-476c-9513-c03de0f162b1",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisiti\n",
        "\n",
        "Prima di iniziare questa esercitazione, assicuratevi di aver installato quanto segue:\n",
        "\n",
        "* Qiskit SDK v2.0 o versioni successive, con supporto [alla visualizzazione](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.40 o più tardi (`pip install qiskit-ibm-runtime`)\n",
        "* Categoria codificatori 2.8.1 (`pip install category-encoders`)\n",
        "* NumPy 2.3.2 (`pip install numpy`)\n",
        "* Pandas 2.3.2 (`pip install pandas`)\n",
        "* Scikit-learn 1.7.1 (`pip install scikit-learn`)\n",
        "* Tqdm 4.67.1 (`pip install tqdm`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2c676996-1361-4b3a-9c94-4784376097b0",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configura\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "fe8a02d3-994a-45fe-823d-5e68ded0d717",
      "metadata": {},
      "outputs": [],
      "source": [
        "import warnings\n",
        "\n",
        "# Standard libraries\n",
        "import os\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "\n",
        "# Machine learning and data processing\n",
        "import category_encoders as ce\n",
        "from scipy.linalg import inv, sqrtm\n",
        "from sklearn.metrics.pairwise import rbf_kernel\n",
        "from sklearn.model_selection import GridSearchCV, StratifiedKFold\n",
        "from sklearn.svm import SVC\n",
        "\n",
        "# Qiskit and IBM Quantum Compute Service\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.circuit import ParameterVector\n",
        "from qiskit.circuit.library import UnitaryGate, ZZFeatureMap\n",
        "from qiskit.quantum_info import SparsePauliOp, random_unitary\n",
        "from qiskit.transpiler import generate_preset_pass_manager\n",
        "from qiskit_ibm_runtime import (\n",
        "    Batch,\n",
        "    EstimatorOptions,\n",
        "    EstimatorV2 as Estimator,\n",
        "    QiskitRuntimeService,\n",
        ")\n",
        "\n",
        "# Progress bar\n",
        "import tqdm\n",
        "\n",
        "warnings.filterwarnings(\"ignore\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bd92ba56-e9eb-4841-b751-7627f47d756c",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Esempio di simulatore su piccola scala\n",
        "\n",
        "In questo tutorial tralasciamo l'esempio del simulatore su piccola scala, poiché il nostro obiettivo principale è dimostrare come i kernel quantistici proiettati possano essere estesi a sistemi più grandi e all'hardware reale.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0fb8a2fa-64b4-434f-8848-e89a098ba73d",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Esempio di hardware su larga scala\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2f8775c8-81b5-4732-8325-3f12dc96b45d",
      "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"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cb3db2e7-8a3d-4ab1-9e3b-ffc5587dee6d",
      "metadata": {},
      "source": [
        "<span id=\"dataset-preparation\" />\n",
        "\n",
        "### Preparazione del set di dati\n",
        "\n",
        "In questa esercitazione utilizziamo un set di dati biologici reali per un compito di classificazione binaria, generato da Daniels et al. (2022) e scaricabile dal [materiale supplementare](https://www.science.org/doi/full/10.1126/science.abq0225#supplementary-materials) incluso nell'articolo. I dati riguardano le cellule T CAR, che sono cellule T geneticamente modificate utilizzate nell'immunoterapia per il trattamento di alcuni tipi di cancro. I linfociti T, un tipo di cellula immunitaria, vengono modificati in laboratorio per esprimere recettori chimerici dell'antigene (CAR) che mirano a proteine specifiche sulle cellule tumorali. Queste cellule T modificate sono in grado di riconoscere e distruggere le cellule tumorali in modo più efficace. Le caratteristiche dei dati sono i motivi delle cellule T CAR, che si riferiscono alla specifica componente strutturale o funzionale della CAR ingegnerizzata nelle cellule T. Sulla base di questi motivi, il nostro compito è quello di prevedere la citotossicità di una data cellula CAR T, etichettandola come tossica o non tossica.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a2ab0503-1cc9-4512-8f06-12223219cc3e",
      "metadata": {},
      "source": [
        "Di seguito sono riportate le funzioni di aiuto per la preelaborazione di questo set di dati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "f1ce0222-e7c7-451c-a11c-e61425a6bb8e",
      "metadata": {},
      "outputs": [],
      "source": [
        "def preprocess_data(dir_root, args):\n",
        "    \"\"\"\n",
        "    Preprocess the training and test data.\n",
        "    \"\"\"\n",
        "    # Read from the csv files\n",
        "    train_data = pd.read_csv(\n",
        "        os.path.join(dir_root, args[\"file_train_data\"]),\n",
        "        encoding=\"unicode_escape\",\n",
        "        sep=\",\",\n",
        "    )\n",
        "    test_data = pd.read_csv(\n",
        "        os.path.join(dir_root, args[\"file_test_data\"]),\n",
        "        encoding=\"unicode_escape\",\n",
        "        sep=\",\",\n",
        "    )\n",
        "\n",
        "    # Fix the last motif ID\n",
        "    train_data[train_data == 17] = 14\n",
        "    train_data.columns = [\n",
        "        \"Cell Number\",\n",
        "        \"motif\",\n",
        "        \"motif.1\",\n",
        "        \"motif.2\",\n",
        "        \"motif.3\",\n",
        "        \"motif.4\",\n",
        "        \"Nalm 6 Cytotoxicity\",\n",
        "    ]\n",
        "    test_data[test_data == 17] = 14\n",
        "    test_data.columns = [\n",
        "        \"Cell Number\",\n",
        "        \"motif\",\n",
        "        \"motif.1\",\n",
        "        \"motif.2\",\n",
        "        \"motif.3\",\n",
        "        \"motif.4\",\n",
        "        \"Nalm 6 Cytotoxicity\",\n",
        "    ]\n",
        "\n",
        "    # Adjust motif at the third position\n",
        "    if args[\"filter_for_spacer_motif_third_position\"]:\n",
        "        train_data = train_data[\n",
        "            (train_data[\"motif.2\"] == 14) | (train_data[\"motif.2\"] == 0)\n",
        "        ]\n",
        "        test_data = test_data[\n",
        "            (test_data[\"motif.2\"] == 14) | (test_data[\"motif.2\"] == 0)\n",
        "        ]\n",
        "\n",
        "    train_data = train_data[\n",
        "        args[\"motifs_to_use\"] + [args[\"label_name\"], \"Cell Number\"]\n",
        "    ]\n",
        "    test_data = test_data[\n",
        "        args[\"motifs_to_use\"] + [args[\"label_name\"], \"Cell Number\"]\n",
        "    ]\n",
        "\n",
        "    # Adjust motif at the last position\n",
        "    if not args[\"allow_spacer_motif_last_position\"]:\n",
        "        last_motif = args[\"motifs_to_use\"][len(args[\"motifs_to_use\"]) - 1]\n",
        "        train_data = train_data[\n",
        "            (train_data[last_motif] != 14) & (train_data[last_motif] != 0)\n",
        "        ]\n",
        "        test_data = test_data[\n",
        "            (test_data[last_motif] != 14) & (test_data[last_motif] != 0)\n",
        "        ]\n",
        "\n",
        "    # Get the labels\n",
        "    train_labels = np.array(train_data[args[\"label_name\"]])\n",
        "    test_labels = np.array(test_data[args[\"label_name\"]])\n",
        "\n",
        "    # For the classification task use the threshold to binarize labels\n",
        "    train_labels[train_labels > args[\"label_binarization_threshold\"]] = 1\n",
        "    train_labels[train_labels < 1] = args[\"min_label_value\"]\n",
        "    test_labels[test_labels > args[\"label_binarization_threshold\"]] = 1\n",
        "    test_labels[test_labels < 1] = args[\"min_label_value\"]\n",
        "\n",
        "    # Reduce data to just the motifs of interest\n",
        "    train_data = train_data[args[\"motifs_to_use\"]]\n",
        "    test_data = test_data[args[\"motifs_to_use\"]]\n",
        "\n",
        "    # Get the class and motif counts\n",
        "    min_class = np.min(np.unique(np.concatenate([train_data, test_data])))\n",
        "    max_class = np.max(np.unique(np.concatenate([train_data, test_data])))\n",
        "\n",
        "    num_class = max_class - min_class + 1\n",
        "    num_motifs = len(args[\"motifs_to_use\"])\n",
        "    print(str(max_class) + \":\" + str(min_class) + \":\" + str(num_class))\n",
        "\n",
        "    train_data = train_data - min_class\n",
        "    test_data = test_data - min_class\n",
        "\n",
        "    return (\n",
        "        train_data,\n",
        "        test_data,\n",
        "        train_labels,\n",
        "        test_labels,\n",
        "        num_class,\n",
        "        num_motifs,\n",
        "    )\n",
        "\n",
        "\n",
        "def data_encoder(args, train_data, test_data, num_class, num_motifs):\n",
        "    \"\"\"\n",
        "    Use one-hot or binary encoding for classical data representation.\n",
        "    \"\"\"\n",
        "    if args[\"encoder\"] == \"one-hot\":\n",
        "        # Transform to one-hot encoding\n",
        "        train_data = np.eye(num_class)[train_data]\n",
        "        test_data = np.eye(num_class)[test_data]\n",
        "\n",
        "        train_data = train_data.reshape(\n",
        "            train_data.shape[0], train_data.shape[1] * train_data.shape[2]\n",
        "        )\n",
        "        test_data = test_data.reshape(\n",
        "            test_data.shape[0], test_data.shape[1] * test_data.shape[2]\n",
        "        )\n",
        "\n",
        "    elif args[\"encoder\"] == \"binary\":\n",
        "        # Transform to binary encoding\n",
        "        encoder = ce.BinaryEncoder()\n",
        "\n",
        "        base_array = np.unique(np.concatenate([train_data, test_data]))\n",
        "        base = pd.DataFrame(base_array).astype(\"category\")\n",
        "        base.columns = [\"motif\"]\n",
        "        for motif_name in args[\"motifs_to_use\"][1:]:\n",
        "            base[motif_name] = base.loc[:, \"motif\"]\n",
        "        encoder.fit(base)\n",
        "\n",
        "        train_data = encoder.transform(train_data.astype(\"category\"))\n",
        "        test_data = encoder.transform(test_data.astype(\"category\"))\n",
        "\n",
        "        train_data = np.reshape(\n",
        "            train_data.values, (train_data.shape[0], num_motifs, -1)\n",
        "        )\n",
        "        test_data = np.reshape(\n",
        "            test_data.values, (test_data.shape[0], num_motifs, -1)\n",
        "        )\n",
        "\n",
        "        train_data = train_data.reshape(\n",
        "            train_data.shape[0], train_data.shape[1] * train_data.shape[2]\n",
        "        )\n",
        "        test_data = test_data.reshape(\n",
        "            test_data.shape[0], test_data.shape[1] * test_data.shape[2]\n",
        "        )\n",
        "\n",
        "    else:\n",
        "        raise ValueError(\"Invalid encoding type.\")\n",
        "\n",
        "    return train_data, test_data"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d72e1c86-4855-4a5a-865b-14ba6b15afb7",
      "metadata": {},
      "source": [
        "È possibile eseguire questa esercitazione eseguendo la seguente cella, che crea automaticamente la struttura di cartelle richiesta e scarica i file di addestramento e di prova direttamente nel vostro ambiente. Se questi file sono già presenti in locale, questo passaggio li sovrascrive in modo sicuro per garantire la coerenza delle versioni.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "84495ee1-880a-48cb-a904-d83396e8b29e",
      "metadata": {},
      "outputs": [],
      "source": [
        "## Download dataset\n",
        "\n",
        "# Create data directory if it doesn't exist\n",
        "!mkdir -p data_tutorial/pqk\n",
        "\n",
        "# Download the training and test sets from the official Qiskit documentation repo\n",
        "!wget -q --show-progress -O data_tutorial/pqk/train_data.csv \\\n",
        "  https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/train_data.csv\n",
        "\n",
        "!wget -q --show-progress -O data_tutorial/pqk/test_data.csv \\\n",
        "  https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/test_data.csv\n",
        "\n",
        "!wget -q --show-progress -O data_tutorial/pqk/projections_train.csv \\\n",
        "  https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/projections_train.csv\n",
        "\n",
        "!wget -q --show-progress -O data_tutorial/pqk/projections_test.csv \\\n",
        "  https://raw.githubusercontent.com/Qiskit/documentation/main/datasets/tutorials/pqk/projections_test.csv\n",
        "\n",
        "# Check the files have been downloaded\n",
        "!echo \"Dataset files downloaded:\"\n",
        "!ls -lh data_tutorial/pqk/*.csv"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "35013bc5-6b5e-44c8-8a8c-3af313b00a82",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "14:0:15\n"
          ]
        }
      ],
      "source": [
        "args = {\n",
        "    \"file_train_data\": \"train_data.csv\",\n",
        "    \"file_test_data\": \"test_data.csv\",\n",
        "    \"motifs_to_use\": [\"motif\", \"motif.1\", \"motif.2\", \"motif.3\"],\n",
        "    \"label_name\": \"Nalm 6 Cytotoxicity\",\n",
        "    \"label_binarization_threshold\": 0.62,\n",
        "    \"filter_for_spacer_motif_third_position\": False,\n",
        "    \"allow_spacer_motif_last_position\": True,\n",
        "    \"min_label_value\": -1,\n",
        "    \"encoder\": \"one-hot\",\n",
        "}\n",
        "dir_root = \"./\"\n",
        "\n",
        "# Preprocess data\n",
        "train_data, test_data, train_labels, test_labels, num_class, num_motifs = (\n",
        "    preprocess_data(dir_root=dir_root, args=args)\n",
        ")\n",
        "\n",
        "# Encode the data\n",
        "train_data, test_data = data_encoder(\n",
        "    args, train_data, test_data, num_class, num_motifs\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "050515c2-64fe-43ce-8559-b58db58b76c3",
      "metadata": {},
      "source": [
        "Trasformiamo inoltre il set di dati in modo che $1$ sia rappresentato come $\\pi/2$ per motivi di scala.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "d40d9a0f-67d0-4704-8a94-cc0a466ffc92",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Change 1 to pi/2\n",
        "angle = np.pi / 2\n",
        "\n",
        "tmp = pd.DataFrame(train_data).astype(\"float64\")\n",
        "tmp[tmp == 1] = angle\n",
        "train_data = tmp.values\n",
        "\n",
        "tmp = pd.DataFrame(test_data).astype(\"float64\")\n",
        "tmp[tmp == 1] = angle\n",
        "test_data = tmp.values"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "726fbdfc-677b-41b7-86c8-dc11adb1946c",
      "metadata": {},
      "source": [
        "Verifichiamo le dimensioni e le forme dei dataset di allenamento e di test.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "b98495f3-aeaa-4df1-a9fe-433e26aa7d4e",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "(172, 60) (172,)\n",
            "(74, 60) (74,)\n"
          ]
        }
      ],
      "source": [
        "print(train_data.shape, train_labels.shape)\n",
        "print(test_data.shape, test_labels.shape)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0c828dc0-9bd1-44bc-b299-303766ae3d37",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-hardware-execution\" />\n",
        "\n",
        "### Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55afd26f-7a53-43bc-920d-88160a61688e",
      "metadata": {},
      "source": [
        "<span id=\"quantum-circuit\" />\n",
        "\n",
        "### Circuito quantistico\n",
        "\n",
        "Costruiamo ora la mappa delle caratteristiche che incorpora il nostro insieme di dati classici in uno spazio di caratteristiche a più alta dimensione. Per questo incorporamento, utilizziamo il metodo [`ZZFeatureMap`](/docs/api/qiskit/qiskit.circuit.library.ZZFeatureMap) di Qiskit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "45956df4-5472-4394-a3e1-5514c456791d",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/projected-quantum-kernels/extracted-outputs/45956df4-5472-4394-a3e1-5514c456791d-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "feature_dimension = train_data.shape[1]\n",
        "reps = 24\n",
        "insert_barriers = True\n",
        "entanglement = \"pairwise\"\n",
        "\n",
        "# ZZFeatureMap with linear entanglement and a repetition of 2\n",
        "embed = ZZFeatureMap(\n",
        "    feature_dimension=feature_dimension,\n",
        "    reps=reps,\n",
        "    entanglement=entanglement,\n",
        "    insert_barriers=insert_barriers,\n",
        "    name=\"ZZFeatureMap\",\n",
        ")\n",
        "embed.decompose().draw(output=\"mpl\", style=\"iqp\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bae2e554-2ca2-4e71-a313-b6aec0b25e6a",
      "metadata": {},
      "source": [
        "Un'altra opzione di incorporazione quantistica è l'ansatz di evoluzione hamiltoniana 1D-Heisenberg. Si può saltare l'esecuzione di questa sezione se si desidera continuare con il sito `ZZFeatureMap`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "659dbf23-fd3f-4e01-94b4-33e6d672172c",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/projected-quantum-kernels/extracted-outputs/659dbf23-fd3f-4e01-94b4-33e6d672172c-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 7,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "feature_dimension = train_data.shape[1]\n",
        "num_qubits = feature_dimension + 1\n",
        "embed2 = QuantumCircuit(num_qubits)\n",
        "num_trotter_steps = 6\n",
        "pv_length = feature_dimension * num_trotter_steps\n",
        "pv = ParameterVector(\"theta\", pv_length)\n",
        "\n",
        "# Add Haar random single qubit unitary to each qubit as initial state\n",
        "np.random.seed(42)\n",
        "seeds_unitary = np.random.randint(0, 100, num_qubits)\n",
        "for i in range(num_qubits):\n",
        "    rand_gate = UnitaryGate(random_unitary(2, seed=seeds_unitary[i]))\n",
        "    embed2.append(rand_gate, [i])\n",
        "\n",
        "\n",
        "def trotter_circ(feature_dimension, num_trotter_steps):\n",
        "    num_qubits = feature_dimension + 1\n",
        "    circ = QuantumCircuit(num_qubits)\n",
        "    # Even\n",
        "    for i in range(0, feature_dimension, 2):\n",
        "        circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)\n",
        "    for i in range(0, feature_dimension, 2):\n",
        "        circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)\n",
        "    for i in range(0, feature_dimension, 2):\n",
        "        circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)\n",
        "    # Odd\n",
        "    for i in range(1, feature_dimension, 2):\n",
        "        circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)\n",
        "    for i in range(1, feature_dimension, 2):\n",
        "        circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)\n",
        "    for i in range(1, feature_dimension, 2):\n",
        "        circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)\n",
        "    return circ\n",
        "\n",
        "\n",
        "# Hamiltonian evolution ansatz\n",
        "for step in range(num_trotter_steps):\n",
        "    circ = trotter_circ(feature_dimension, num_trotter_steps)\n",
        "    if step % 2 == 0:\n",
        "        embed2 = embed2.compose(circ)\n",
        "    else:\n",
        "        reverse_circ = circ.reverse_ops()\n",
        "        embed2 = embed2.compose(reverse_circ)\n",
        "\n",
        "\n",
        "embed2.draw(output=\"mpl\", style=\"iqp\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8b27a3f8-5ee9-41b5-a430-25247545cbb9",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### Passaggio 3: eseguire utilizzando Qiskit primitives\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f202d995-8fc0-4af5-b11e-2ed72ac48c84",
      "metadata": {},
      "source": [
        "<span id=\"measure-1-rdms\" />\n",
        "\n",
        "### Misura 1-RDM\n",
        "\n",
        "In questa fase, otteniamo tutte le matrici di densità ridotte a un singolo qubit (1-RDM) tramite misurazioni proiettive della mappa delle caratteristiche quantistiche, che saranno successivamente inserite nella funzione kernel esponenziale classica.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ea823c34-d7d7-42f8-989c-dfe78cdd489a",
      "metadata": {},
      "source": [
        "Vediamo come calcolare l'1-RDM con un singolo punto del set di dati prima di analizzare tutti i dati. Gli 1-RDM sono una collezione di misure a singolo qubit degli operatori di Pauli `X`, `Y` e `Z` su tutti i qubit. Questo perché un RDM a singolo qubit può essere espresso completamente come: $\\rho = \\frac{1}{2} \\big( I + \\braket \\sigma_x \\sigma_x  + \\braket \\sigma_y \\sigma_y + \\braket \\sigma_z \\sigma_z  \\big)$\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "daa7608c-f482-4df5-a2be-6ca9625bb7e0",
      "metadata": {},
      "source": [
        "Per prima cosa selezioniamo il backend da utilizzare.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e1ab9cea-42ef-478c-bb9d-02ed4cf23ea6",
      "metadata": {},
      "outputs": [],
      "source": [
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=133\n",
        ")\n",
        "target = backend.target"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2ce0917f-3826-477b-b503-57e3b5e7e290",
      "metadata": {},
      "source": [
        "Quindi si esegue il circuito quantistico e si misurano le proiezioni. Si noti che è stata attivata la mitigazione degli errori, compresa l'estrapolazione a rumore zero (ZNE).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "53b20cec-ef8a-4fdb-aeed-46546a32ea96",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Let's select the ZZFeatureMap embedding for this example\n",
        "qc = embed\n",
        "num_qubits = feature_dimension\n",
        "\n",
        "# Identity operator on all qubits\n",
        "id = \"I\" * num_qubits\n",
        "\n",
        "# Let's select the first training datapoint as an example\n",
        "parameters = train_data[0]\n",
        "\n",
        "# Bind parameter to the circuit and simplify it\n",
        "qc_bound = qc.assign_parameters(parameters)\n",
        "transpiler = generate_preset_pass_manager(\n",
        "    optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n",
        ")\n",
        "transpiled_circuit = transpiler.run(qc_bound)\n",
        "\n",
        "# Transpile for hardware\n",
        "transpiler = generate_preset_pass_manager(optimization_level=3, target=target)\n",
        "transpiled_circuit = transpiler.run(transpiled_circuit)\n",
        "\n",
        "# We group all commuting observables\n",
        "# These groups are the Pauli X, Y and Z operators on individual qubits\n",
        "observables_x = [\n",
        "    SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n",
        "        transpiled_circuit.layout\n",
        "    )\n",
        "    for i in range(num_qubits)\n",
        "]\n",
        "observables_y = [\n",
        "    SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n",
        "        transpiled_circuit.layout\n",
        "    )\n",
        "    for i in range(num_qubits)\n",
        "]\n",
        "observables_z = [\n",
        "    SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n",
        "        transpiled_circuit.layout\n",
        "    )\n",
        "    for i in range(num_qubits)\n",
        "]\n",
        "\n",
        "# We define the primitive unified blocs (PUBs) consisting of the embedding circuit,\n",
        "# set of observables and the circuit parameters\n",
        "pub_x = (transpiled_circuit, observables_x)\n",
        "pub_y = (transpiled_circuit, observables_y)\n",
        "pub_z = (transpiled_circuit, observables_z)\n",
        "\n",
        "# Experiment options for error mitigation\n",
        "num_randomizations = 300\n",
        "shots_per_randomization = 100\n",
        "noise_factors = [1, 3, 5]\n",
        "\n",
        "experimental_opts = {}\n",
        "experimental_opts[\"resilience\"] = {\n",
        "    \"measure_mitigation\": True,\n",
        "    \"zne_mitigation\": True,\n",
        "    \"zne\": {\n",
        "        \"noise_factors\": noise_factors,\n",
        "        \"amplifier\": \"gate_folding\",\n",
        "        \"extrapolated_noise_factors\": [0] + noise_factors,\n",
        "    },\n",
        "}\n",
        "experimental_opts[\"twirling\"] = {\n",
        "    \"num_randomizations\": num_randomizations,\n",
        "    \"shots_per_randomization\": shots_per_randomization,\n",
        "    \"strategy\": \"active-accum\",\n",
        "}\n",
        "\n",
        "# We define and run the estimator to obtain <X>, <Y> and <Z> on all qubits\n",
        "estimator = Estimator(mode=backend, options=experimental_opts)\n",
        "\n",
        "job = estimator.run([pub_x, pub_y, pub_z])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b84baa9d-22a7-410e-a424-2b34e57ff96e",
      "metadata": {},
      "source": [
        "Quindi recuperiamo i risultati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "7a56eda8-3fb2-43e9-8a82-ec64be05b699",
      "metadata": {},
      "outputs": [],
      "source": [
        "job_result_x = job.result()[0].data.evs\n",
        "job_result_y = job.result()[1].data.evs\n",
        "job_result_z = job.result()[2].data.evs"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "1bf21466-ac70-4172-841b-08cedf835645",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[ 3.67865951e-03  1.01158571e-02 -3.95790878e-02  6.33984326e-03\n",
            "  1.86035759e-02 -2.91533268e-02 -1.06374793e-01  4.48873518e-18\n",
            "  4.70201764e-02  3.53997968e-02  2.53130819e-02  3.23903401e-02\n",
            "  6.06327843e-03  1.16313667e-02 -1.12387504e-02 -3.18457725e-02\n",
            " -4.16445718e-04 -1.45609602e-03 -4.21737114e-01  2.83705669e-02\n",
            "  6.91332890e-03 -7.45363001e-02 -1.20139326e-02 -8.85566135e-02\n",
            " -3.22648394e-02 -3.24228074e-02  6.20431299e-04  3.04225434e-03\n",
            "  5.72795792e-03  1.11288428e-02  1.50395861e-01  9.18380197e-02\n",
            "  1.02553163e-01  2.98312847e-02 -3.30298912e-01 -1.13979648e-01\n",
            "  4.49159340e-03  8.63861493e-02  3.05666566e-02  2.21463145e-04\n",
            "  1.45946735e-02  8.54537275e-03 -8.09805979e-02 -2.92608104e-02\n",
            " -3.91243644e-02 -3.96632760e-02 -1.41187613e-01 -1.07363243e-01\n",
            "  1.81089440e-02  2.70778895e-02  1.45139414e-02  2.99480458e-02\n",
            "  4.99137134e-02  7.08789852e-02  4.30565759e-02  8.71287156e-02\n",
            "  1.04334798e-01  7.72191962e-02  7.10059720e-02  1.04650403e-01]\n",
            "[-7.31765102e-05  7.42669174e-03  9.82277344e-03  5.92638249e-02\n",
            "  4.24120486e-02 -9.06473416e-03  4.55057675e-03  8.43494094e-03\n",
            "  6.92097339e-02 -6.82234424e-02  6.13509008e-02  3.94200491e-02\n",
            " -1.24037979e-02  1.01976642e-01  7.90538600e-03 -7.19726160e-02\n",
            " -1.19501703e-16 -1.03796614e-02  7.37382463e-02  1.97238568e-01\n",
            " -3.59250635e-02 -2.67554009e-02  3.55010633e-02  7.68877990e-02\n",
            "  6.50677589e-05 -6.59298767e-03 -1.23719487e-02 -6.41938151e-02\n",
            "  1.95603072e-02 -2.48448551e-02  5.17784810e-02 -5.93767100e-02\n",
            "  3.11897681e-02 -3.91959720e-18 -4.47769148e-03  1.39202197e-01\n",
            " -6.56387523e-02 -5.85665483e-02  9.52905894e-03 -8.61460731e-02\n",
            "  3.91790656e-02 -1.27544375e-01  1.63712244e-01  3.36816934e-04\n",
            "  2.26230028e-02 -2.45023393e-05  4.95635588e-03  1.44779564e-01\n",
            "  3.71625177e-02  3.65675948e-03  2.83694017e-02 -7.10500602e-02\n",
            " -1.15467702e-01  6.21712129e-03 -4.80958959e-02  2.21021066e-02\n",
            "  7.99062499e-02 -1.87164076e-02 -3.67100369e-02 -2.38923731e-02]\n",
            "[ 6.85871605e-01  5.07725024e-01  8.71024642e-03  3.34823455e-02\n",
            "  4.58684961e-02  9.44384189e-17 -4.46829296e-02 -2.91296778e-02\n",
            "  4.15466461e-02  2.89628330e-02  1.88624017e-03  5.37110446e-02\n",
            "  2.59579053e-03  1.39327071e-02 -2.90781778e-02  5.07209866e-03\n",
            "  5.83403000e-02  2.60764440e-02  4.45999706e-17 -6.66701417e-03\n",
            "  3.03215873e-01  2.26172533e-02  2.43105960e-02  4.98861041e-18\n",
            " -2.45530791e-02  6.26940708e-02  1.21058073e-02  2.76675948e-04\n",
            "  2.63980996e-02  2.58302364e-02  7.47856723e-02  8.42728943e-02\n",
            "  5.70989097e-02  6.92955086e-02 -5.68313712e-03  1.32199452e-01\n",
            "  8.90511238e-02 -3.45204621e-02 -1.05445836e-01  6.03864150e-03\n",
            "  2.16291384e-02  8.22303162e-03  1.00856715e-02  6.28973151e-02\n",
            "  6.26727169e-02  6.15399206e-02  9.67320897e-02  1.03045269e-16\n",
            "  1.79688783e-01 -1.59960520e-02 -1.15422952e-02  9.60200470e-03\n",
            "  6.58396672e-02  7.78329830e-03  6.53226955e-02  2.45778685e-03\n",
            "  4.36694753e-03  5.75098762e-03 -2.48896201e-02  8.33740755e-05]\n"
          ]
        }
      ],
      "source": [
        "print(job_result_x)\n",
        "print(job_result_y)\n",
        "print(job_result_z)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d2772c67-e30b-4e4c-8884-d03c88fe078f",
      "metadata": {},
      "source": [
        "Stampiamo la dimensione del circuito e la profondità del gate a due qubit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "4f573436-ec5c-451b-976c-ad718b3c201d",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "qubits: 60\n",
            "2q-depth: 64\n",
            "2q-size: 1888\n",
            "Operator counts: OrderedDict({'rz': 6016, 'sx': 4576, 'cz': 1888, 'x': 896, 'barrier': 31})\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/projected-quantum-kernels/extracted-outputs/4f573436-ec5c-451b-976c-ad718b3c201d-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 13,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "print(f\"qubits: {qc.num_qubits}\")\n",
        "print(\n",
        "    f\"2q-depth: {transpiled_circuit.depth(lambda x: x.operation.num_qubits==2)}\"\n",
        ")\n",
        "print(\n",
        "    f\"2q-size: {transpiled_circuit.size(lambda x: x.operation.num_qubits==2)}\"\n",
        ")\n",
        "print(f\"Operator counts: {transpiled_circuit.count_ops()}\")\n",
        "transpiled_circuit.draw(\"mpl\", fold=-1, style=\"clifford\", idle_wires=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b03a7f5a-4dae-4773-b372-fe04570ad2cd",
      "metadata": {},
      "source": [
        "A questo punto è possibile eseguire un ciclo sull'intero set di dati di allenamento per ottenere tutti gli 1-RDM.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "eabb625e-edc1-445f-ad14-5e472d8a2879",
      "metadata": {},
      "source": [
        "Forniamo anche i risultati di un esperimento condotto su hardware quantistico. È possibile eseguire l'addestramento autonomamente impostando il flag sottostante su `True`, oppure utilizzare i risultati delle proiezioni che forniamo.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "81835ec2-210f-4176-ba79-c8046cc57d92",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Set this to True if you want to run the training on hardware\n",
        "run_experiment = False"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "932201c0-178b-4a98-b2dc-5b4c81953d49",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "Training data progress: 100%|██████████| 172/172 [13:03<00:00,  4.55s/it]\n"
          ]
        }
      ],
      "source": [
        "# Identity operator on all qubits\n",
        "id = \"I\" * num_qubits\n",
        "\n",
        "# projections_train[i][j][k] will be the expectation value of the j-th\n",
        "# Pauli operator (0: X, 1: Y, 2: Z) of datapoint i on qubit k\n",
        "projections_train = []\n",
        "jobs_train = []\n",
        "\n",
        "# Experiment options for error mitigation\n",
        "num_randomizations = 300\n",
        "shots_per_randomization = 100\n",
        "noise_factors = [1, 3, 5]\n",
        "\n",
        "experimental_opts = {}\n",
        "experimental_opts[\"resilience\"] = {\n",
        "    \"measure_mitigation\": True,\n",
        "    \"zne_mitigation\": True,\n",
        "    \"zne\": {\n",
        "        \"noise_factors\": noise_factors,\n",
        "        \"amplifier\": \"gate_folding\",\n",
        "        \"return_all_extrapolated\": True,\n",
        "        \"return_unextrapolated\": True,\n",
        "        \"extrapolated_noise_factors\": [0] + noise_factors,\n",
        "    },\n",
        "}\n",
        "experimental_opts[\"twirling\"] = {\n",
        "    \"num_randomizations\": num_randomizations,\n",
        "    \"shots_per_randomization\": shots_per_randomization,\n",
        "    \"strategy\": \"active-accum\",\n",
        "}\n",
        "options = EstimatorOptions(experimental=experimental_opts)\n",
        "\n",
        "if run_experiment:\n",
        "    with Batch(backend=backend):\n",
        "        for i in tqdm.tqdm(\n",
        "            range(len(train_data)), desc=\"Training data progress\"\n",
        "        ):\n",
        "            # Get training sample\n",
        "            parameters = train_data[i]\n",
        "\n",
        "            # Bind parameter to the circuit and simplify it\n",
        "            qc_bound = qc.assign_parameters(parameters)\n",
        "            transpiler = generate_preset_pass_manager(\n",
        "                optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n",
        "            )\n",
        "            transpiled_circuit = transpiler.run(qc_bound)\n",
        "\n",
        "            # Transpile for hardware\n",
        "            transpiler = generate_preset_pass_manager(\n",
        "                optimization_level=3, target=target\n",
        "            )\n",
        "            transpiled_circuit = transpiler.run(transpiled_circuit)\n",
        "\n",
        "            # We group all commuting observables\n",
        "            # These groups are the Pauli X, Y and Z operators on individual qubits\n",
        "            observables_x = [\n",
        "                SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n",
        "                    transpiled_circuit.layout\n",
        "                )\n",
        "                for i in range(num_qubits)\n",
        "            ]\n",
        "            observables_y = [\n",
        "                SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n",
        "                    transpiled_circuit.layout\n",
        "                )\n",
        "                for i in range(num_qubits)\n",
        "            ]\n",
        "            observables_z = [\n",
        "                SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n",
        "                    transpiled_circuit.layout\n",
        "                )\n",
        "                for i in range(num_qubits)\n",
        "            ]\n",
        "\n",
        "            # We define the primitive unified blocs (PUBs) consisting\n",
        "            # of the embedding circuit,\n",
        "            # set of observables and the circuit parameters\n",
        "            pub_x = (transpiled_circuit, observables_x)\n",
        "            pub_y = (transpiled_circuit, observables_y)\n",
        "            pub_z = (transpiled_circuit, observables_z)\n",
        "\n",
        "            # We define and run the estimator to obtain <X>, <Y> and <Z>\n",
        "            # on all qubits\n",
        "            estimator = Estimator(options=options)\n",
        "\n",
        "            job = estimator.run([pub_x, pub_y, pub_z])\n",
        "            jobs_train.append(job)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b401f9a9-c2a2-4454-9708-c53e0cbf122b",
      "metadata": {},
      "source": [
        "Una volta completati i lavori, possiamo recuperare i risultati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ccbd7603-1dd3-4aab-8ee8-8b0a98068b61",
      "metadata": {},
      "outputs": [],
      "source": [
        "if run_experiment:\n",
        "    for i in tqdm.tqdm(\n",
        "        range(len(train_data)), desc=\"Retrieving training data results\"\n",
        "    ):\n",
        "        # Completed job\n",
        "        job = jobs_train[i]\n",
        "\n",
        "        # Job results\n",
        "        job_result_x = job.result()[0].data.evs\n",
        "        job_result_y = job.result()[1].data.evs\n",
        "        job_result_z = job.result()[2].data.evs\n",
        "\n",
        "        # Record <X>, <Y> and <Z> on all qubits for the current datapoint\n",
        "        projections_train.append([job_result_x, job_result_y, job_result_z])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b03a29c5-c504-4ba9-9eda-4c8bc8817492",
      "metadata": {},
      "source": [
        "Ripetiamo l'operazione per il set di test.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f9e77a9c-d295-4893-aebe-74cc59168e1f",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "Test data progress: 100%|██████████| 74/74 [00:13<00:00,  5.56it/s]\n"
          ]
        }
      ],
      "source": [
        "# Identity operator on all qubits\n",
        "id = \"I\" * num_qubits\n",
        "\n",
        "# projections_test[i][j][k] will be the expectation value of the\n",
        "# j-th Pauli operator (0: X, 1: Y, 2: Z) of datapoint i on qubit k\n",
        "projections_test = []\n",
        "jobs_test = []\n",
        "\n",
        "# Experiment options for error mitigation\n",
        "num_randomizations = 300\n",
        "shots_per_randomization = 100\n",
        "noise_factors = [1, 3, 5]\n",
        "\n",
        "experimental_opts = {}\n",
        "experimental_opts[\"resilience\"] = {\n",
        "    \"measure_mitigation\": True,\n",
        "    \"zne_mitigation\": True,\n",
        "    \"zne\": {\n",
        "        \"noise_factors\": noise_factors,\n",
        "        \"amplifier\": \"gate_folding\",\n",
        "        \"return_all_extrapolated\": True,\n",
        "        \"return_unextrapolated\": True,\n",
        "        \"extrapolated_noise_factors\": [0] + noise_factors,\n",
        "    },\n",
        "}\n",
        "experimental_opts[\"twirling\"] = {\n",
        "    \"num_randomizations\": num_randomizations,\n",
        "    \"shots_per_randomization\": shots_per_randomization,\n",
        "    \"strategy\": \"active-accum\",\n",
        "}\n",
        "options = EstimatorOptions(experimental=experimental_opts)\n",
        "\n",
        "if run_experiment:\n",
        "    with Batch(backend=backend):\n",
        "        for i in tqdm.tqdm(range(len(test_data)), desc=\"Test data progress\"):\n",
        "            # Get test sample\n",
        "            parameters = test_data[i]\n",
        "\n",
        "            # Bind parameter to the circuit and simplify it\n",
        "            qc_bound = qc.assign_parameters(parameters)\n",
        "            transpiler = generate_preset_pass_manager(\n",
        "                optimization_level=3, basis_gates=[\"u3\", \"cz\"]\n",
        "            )\n",
        "            transpiled_circuit = transpiler.run(qc_bound)\n",
        "\n",
        "            # Transpile for hardware\n",
        "            transpiler = generate_preset_pass_manager(\n",
        "                optimization_level=3, target=target\n",
        "            )\n",
        "            transpiled_circuit = transpiler.run(transpiled_circuit)\n",
        "\n",
        "            # We group all commuting observables\n",
        "            # These groups are the Pauli X, Y and Z operators on individual qubits\n",
        "            observables_x = [\n",
        "                SparsePauliOp(id[:i] + \"X\" + id[(i + 1) :]).apply_layout(\n",
        "                    transpiled_circuit.layout\n",
        "                )\n",
        "                for i in range(num_qubits)\n",
        "            ]\n",
        "            observables_y = [\n",
        "                SparsePauliOp(id[:i] + \"Y\" + id[(i + 1) :]).apply_layout(\n",
        "                    transpiled_circuit.layout\n",
        "                )\n",
        "                for i in range(num_qubits)\n",
        "            ]\n",
        "            observables_z = [\n",
        "                SparsePauliOp(id[:i] + \"Z\" + id[(i + 1) :]).apply_layout(\n",
        "                    transpiled_circuit.layout\n",
        "                )\n",
        "                for i in range(num_qubits)\n",
        "            ]\n",
        "\n",
        "            # We define the primitive unified blocs (PUBs) consisting of\n",
        "            # the embedding circuit,\n",
        "            # set of observables and the circuit parameters\n",
        "            pub_x = (transpiled_circuit, observables_x)\n",
        "            pub_y = (transpiled_circuit, observables_y)\n",
        "            pub_z = (transpiled_circuit, observables_z)\n",
        "\n",
        "            # We define and run the estimator to obtain <X>, <Y> and <Z> on all qubits\n",
        "            estimator = Estimator(options=options)\n",
        "\n",
        "            job = estimator.run([pub_x, pub_y, pub_z])\n",
        "            jobs_test.append(job)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7190af84-d419-4dad-b566-0d66c96e320d",
      "metadata": {},
      "source": [
        "Possiamo recuperare i risultati come prima.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "721ed991-fee2-4f66-9bb6-a750ee935033",
      "metadata": {},
      "outputs": [],
      "source": [
        "if run_experiment:\n",
        "    for i in tqdm.tqdm(\n",
        "        range(len(test_data)), desc=\"Retrieving test data results\"\n",
        "    ):\n",
        "        # Completed job\n",
        "        job = jobs_test[i]\n",
        "\n",
        "        # Job results\n",
        "        job_result_x = job.result()[0].data.evs\n",
        "        job_result_y = job.result()[1].data.evs\n",
        "        job_result_z = job.result()[2].data.evs\n",
        "\n",
        "        # Record <X>, <Y> and <Z> on all qubits for the current datapoint\n",
        "        projections_test.append([job_result_x, job_result_y, job_result_z])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b87be787-8751-4093-9547-57315fa13c88",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7e10f152-d20c-4c70-9530-e9e71c309b59",
      "metadata": {},
      "source": [
        "<span id=\"define-the-projected-quantum-kernel\" />\n",
        "\n",
        "### Definire il kernel quantistico previsto\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "34150c16-e6ca-43a3-989d-444104dc60e5",
      "metadata": {},
      "source": [
        "Il kernel quantistico proiettato è definito con la seguente funzione kernel: $k^{\\textrm{PQ}}(x_i, x_j) = \\textrm{exp} \\Big(-\\gamma \\sum_k \\sum_{P \\in \\{ X,Y,Z \\}} (\\textrm{Tr}[P \\rho_k(x_i)] - \\textrm{Tr}[P \\rho_k(x_j)])^2 \\Big) $ Nell'equazione precedente, $\\gamma>0$ è un iperparametro regolabile. $K^{\\textrm{PQ}}_{ij} = k^{\\textrm{PQ}}(x_i, x_j)$ sono le voci della matrice kernel $K^{\\textrm{PQ}}$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "42cea21f-3178-4ac9-8826-db7ca7cdfe20",
      "metadata": {},
      "source": [
        "Utilizzando la definizione di 1-RDM, possiamo vedere che i singoli termini all'interno della funzione kernel possono essere valutati come $\\textrm{Tr}[P \\rho_k (x_i)] = \\braket P$, dove $P \\in \\{ X,Y,Z \\}$. Questi valori di aspettativa sono proprio quelli che abbiamo misurato in precedenza.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "22e41ed2-93be-4b30-a898-8d2832d1e0ab",
      "metadata": {},
      "source": [
        "Utilizzando `scikit-learn`, possiamo infatti calcolare il kernel in modo ancora più semplice. Ciò è dovuto al kernel a base radiale (`'rbf'`) facilmente disponibile: $ \\textrm{exp} (-\\gamma \\lVert x - x' \\rVert^2)$. Per prima cosa, è sufficiente rimodellare i nuovi set di dati di allenamento e di test proiettati in array bidimensionali.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e01abd8a-2194-41b9-adaf-7ec80f14e3b1",
      "metadata": {},
      "source": [
        "Si noti che l'analisi dell'intero set di dati può richiedere circa 80 minuti sulla QPU. Per assicurarci che il resto del tutorial sia facilmente eseguibile, forniamo anche le proiezioni di un esperimento eseguito in precedenza (incluse nei file scaricati nel blocco di codice `Download dataset` ). Se avete eseguito la formazione da soli, potete continuare l'esercitazione con i vostri risultati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "a61e6c67-056b-4659-87a0-284bda432cfd",
      "metadata": {},
      "outputs": [],
      "source": [
        "if run_experiment:\n",
        "    projections_train = np.array(projections_train).reshape(\n",
        "        len(projections_train), -1\n",
        "    )\n",
        "    projections_test = np.array(projections_test).reshape(\n",
        "        len(projections_test), -1\n",
        "    )\n",
        "else:\n",
        "    projections_train = np.loadtxt(\"projections_train.txt\")\n",
        "    projections_test = np.loadtxt(\"projections_test.txt\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2c90c625-27c1-46a0-80a0-5ead9d3c64b7",
      "metadata": {},
      "source": [
        "<span id=\"support-vector-machine-svm\" />\n",
        "\n",
        "### Macchina a vettori di supporto (SVM)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b2ea7dde-63ec-4e3d-b6dc-dbeab6a07fda",
      "metadata": {},
      "source": [
        "Ora possiamo eseguire una SVM classica su questo kernel precalcolato e utilizzare il kernel tra gli insiemi di test e di allenamento per la predizione.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "d2eef986-22a5-4528-8fa0-c7dbfd586071",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Fitting 10 folds for each of 6622 candidates, totalling 66220 fits\n",
            "The best parameters are {'C': 8.5, 'gamma': 0.01} with a score of 0.6980\n",
            "Test accuracy with best model: 0.8108\n"
          ]
        }
      ],
      "source": [
        "# Range of 'C' and 'gamma' values as SVC hyperparameters\n",
        "C_range = [0.001, 0.005, 0.007]\n",
        "C_range.extend([x * 0.01 for x in range(1, 11)])\n",
        "C_range.extend([x * 0.25 for x in range(1, 60)])\n",
        "C_range.extend(\n",
        "    [\n",
        "        20,\n",
        "        50,\n",
        "        100,\n",
        "        200,\n",
        "        500,\n",
        "        700,\n",
        "        1000,\n",
        "        1100,\n",
        "        1200,\n",
        "        1300,\n",
        "        1400,\n",
        "        1500,\n",
        "        1700,\n",
        "        2000,\n",
        "    ]\n",
        ")\n",
        "\n",
        "gamma_range = [\"auto\", \"scale\", 0.001, 0.005, 0.007]\n",
        "gamma_range.extend([x * 0.01 for x in range(1, 11)])\n",
        "gamma_range.extend([x * 0.25 for x in range(1, 60)])\n",
        "gamma_range.extend([20, 50, 100])\n",
        "\n",
        "param_grid = dict(C=C_range, gamma=gamma_range)\n",
        "\n",
        "# Support vector classifier\n",
        "svc = SVC(kernel=\"rbf\")\n",
        "\n",
        "# Define the cross validation\n",
        "cv = StratifiedKFold(n_splits=10)\n",
        "\n",
        "# Grid search for hyperparameter tuning (q: quantum)\n",
        "grid_search_q = GridSearchCV(\n",
        "    svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring=\"f1_weighted\"\n",
        ")\n",
        "grid_search_q.fit(projections_train, train_labels)\n",
        "\n",
        "# Best model with best parameters\n",
        "best_svc_q = grid_search_q.best_estimator_\n",
        "print(\n",
        "    f\"The best parameters are {grid_search_q.best_params_} with a score of {grid_search_q.best_score_:.4f}\"\n",
        ")\n",
        "\n",
        "# Test accuracy\n",
        "accuracy_q = best_svc_q.score(projections_test, test_labels)\n",
        "print(f\"Test accuracy with best model: {accuracy_q:.4f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d54cb69a-8f53-43f1-870a-af273f91e47c",
      "metadata": {},
      "source": [
        "<span id=\"classical-benchmarking\" />\n",
        "\n",
        "### Benchmarking classico\n",
        "\n",
        "Possiamo eseguire una SVM classica con la funzione di base radiale come kernel senza eseguire una proiezione quantica. Questo risultato è il nostro classico parametro di riferimento.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "41867b5a-9091-4aa4-adab-a05cf6238966",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Fitting 10 folds for each of 6622 candidates, totalling 66220 fits\n",
            "The best parameters are {'C': 10.75, 'gamma': 0.04} with a score of 0.7830\n",
            "Test accuracy with best model: 0.7432\n"
          ]
        }
      ],
      "source": [
        "# Support vector classifier\n",
        "svc = SVC(kernel=\"rbf\")\n",
        "\n",
        "# Grid search for hyperparameter tuning (c: classical)\n",
        "grid_search_c = GridSearchCV(\n",
        "    svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring=\"f1_weighted\"\n",
        ")\n",
        "grid_search_c.fit(train_data, train_labels)\n",
        "\n",
        "# Best model with best parameters\n",
        "best_svc_c = grid_search_c.best_estimator_\n",
        "print(\n",
        "    f\"The best parameters are {grid_search_c.best_params_} with a score of {grid_search_c.best_score_:.4f}\"\n",
        ")\n",
        "\n",
        "# Test accuracy\n",
        "accuracy_c = best_svc_c.score(test_data, test_labels)\n",
        "print(f\"Test accuracy with best model: {accuracy_c:.4f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5b0d367f-4a15-4269-8012-9baef30422fa",
      "metadata": {},
      "source": [
        "<span id=\"appendix-verify-the-datasets-potential-quantum-advantage-in-learning-tasks\" />\n",
        "\n",
        "## Appendice: Verificare il potenziale vantaggio quantistico del set di dati nelle attività di apprendimento\n",
        "\n",
        "Non tutti i set di dati offrono un potenziale vantaggio dall'uso dei PQK. Esistono alcuni limiti teorici che si possono utilizzare come test preliminare per verificare se un particolare set di dati può trarre beneficio dalle PQK. Per quantificare questo aspetto, gli autori di [Power of data in quantum machine learning](https://www.nature.com/articles/s41467-021-22539-9) \\[2] definiscono quantità denominate complessità dei modelli classici e quantistici e separazione geometrica dei modelli classici e quantistici. Per aspettarsi un potenziale vantaggio quantistico dai PQK, la separazione geometrica tra i kernel classici e quelli proiettati in modo quantistico dovrebbe essere approssimativamente dell'ordine di $\\sqrt{N}$, dove $N$ è il numero di campioni di addestramento. Se questa condizione è soddisfatta, si passa alla verifica della complessità del modello. Se la complessità del modello classico è dell'ordine di $N$ mentre la complessità del modello quantistico proiettato è sostanzialmente inferiore a $N$, possiamo aspettarci un potenziale vantaggio dal PQK.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d8fae838-a34b-4cf5-ba98-579ec8527fde",
      "metadata": {},
      "source": [
        "La separazione geometrica è definita come segue ( F19 in [\\[2\\]](#references) ): $g_{cq} = g(K^c \\Vert K^q) = \\sqrt{\\Vert \\sqrt{K^q} \\sqrt{K^c} (K^c + \\lambda I)^{-2} \\sqrt{K^c} \\sqrt{K^q}\\Vert_{\\infty}}$\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "bc67f5c0-5d79-4633-807e-02b8cc9d39f5",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Geometric separation between classical and quantum kernels is 1.5440\n",
            "13.114877048604\n"
          ]
        }
      ],
      "source": [
        "# Gamma values used in best models above\n",
        "gamma_c = grid_search_c.best_params_[\"gamma\"]\n",
        "gamma_q = grid_search_q.best_params_[\"gamma\"]\n",
        "\n",
        "# Regularization parameter used in the best classical model above\n",
        "C_c = grid_search_c.best_params_[\"C\"]\n",
        "l_c = 1 / C_c\n",
        "\n",
        "# Classical and quantum kernels used above\n",
        "K_c = rbf_kernel(train_data, train_data, gamma=gamma_c)\n",
        "K_q = rbf_kernel(projections_train, projections_train, gamma=gamma_q)\n",
        "\n",
        "# Intermediate matrices in the equation\n",
        "K_c_sqrt = sqrtm(K_c)\n",
        "K_q_sqrt = sqrtm(K_q)\n",
        "K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))\n",
        "K_multiplication = (\n",
        "    K_q_sqrt @ K_c_sqrt @ K_c_inv @ K_c_inv @ K_c_sqrt @ K_q_sqrt\n",
        ")\n",
        "\n",
        "# Geometric separation\n",
        "norm = np.linalg.norm(K_multiplication, ord=np.inf)\n",
        "g_cq = np.sqrt(norm)\n",
        "print(\n",
        "    f\"Geometric separation between classical and quantum kernels is {g_cq:.4f}\"\n",
        ")\n",
        "\n",
        "print(np.sqrt(len(train_data)))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "68ff51a9-d52e-4103-982b-c81bae17d6a9",
      "metadata": {},
      "source": [
        "La complessità del modello è definita come segue ( M1 in [\\[2\\]](#references) ): $ s_{K, \\lambda}(N) = \\sqrt{\\frac{\\lambda^2 \\sum_{i=1}^N \\sum_{j=1}^N (K+\\lambda I)^{-2}_{ij} y_i y_j}{N}} + \\sqrt{\\frac{\\sum_{i=1}^N \\sum_{j=1}^N ((K+\\lambda I)^{-1}K(K+\\lambda I)^{-1})_{ij} y_i y_j}{N}}$\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "877cf344-7324-4154-b0d9-7bcfa03b6ac0",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Classical model complexity is 1.3578\n"
          ]
        }
      ],
      "source": [
        "# Model complexity of the classical kernel\n",
        "\n",
        "# Number of training data\n",
        "N = len(train_data)\n",
        "\n",
        "# Predicted labels\n",
        "pred_labels = best_svc_c.predict(train_data)\n",
        "pred_matrix = np.outer(pred_labels, pred_labels)\n",
        "\n",
        "# Intermediate terms\n",
        "K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))\n",
        "\n",
        "# First term\n",
        "first_sum = np.sum((K_c_inv @ K_c_inv) * pred_matrix)\n",
        "first_term = l_c * np.sqrt(first_sum / N)\n",
        "\n",
        "# Second term\n",
        "second_sum = np.sum((K_c_inv @ K_c @ K_c_inv) * pred_matrix)\n",
        "second_term = np.sqrt(second_sum / N)\n",
        "\n",
        "# Model complexity\n",
        "s_c = first_term + second_term\n",
        "print(f\"Classical model complexity is {s_c:.4f}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "90e9de08-cafc-4493-9358-581c148f3447",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Quantum model complexity is 1.5806\n"
          ]
        }
      ],
      "source": [
        "# Model complexity of the projected quantum kernel\n",
        "\n",
        "# Number of training data\n",
        "N = len(projections_train)\n",
        "\n",
        "# Predicted labels\n",
        "pred_labels = best_svc_q.predict(projections_train)\n",
        "pred_matrix = np.outer(pred_labels, pred_labels)\n",
        "\n",
        "# Regularization parameter used in the best classical model above\n",
        "C_q = grid_search_q.best_params_[\"C\"]\n",
        "l_q = 1 / C_q\n",
        "\n",
        "# Intermediate terms\n",
        "K_q_inv = inv(K_q + l_q * np.eye(K_q.shape[0]))\n",
        "\n",
        "# First term\n",
        "first_sum = np.sum((K_q_inv @ K_q_inv) * pred_matrix)\n",
        "first_term = l_q * np.sqrt(first_sum / N)\n",
        "\n",
        "# Second term\n",
        "second_sum = np.sum((K_q_inv @ K_q @ K_q_inv) * pred_matrix)\n",
        "second_term = np.sqrt(second_sum / N)\n",
        "\n",
        "# Model complexity\n",
        "s_q = first_term + second_term\n",
        "print(f\"Quantum model complexity is {s_q:.4f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e29581c7-ec49-4a03-837f-ee1fe9178846",
      "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 contenuti:\n",
        "\n",
        "  * [Corso](/learning/courses/quantum-machine-learning) approfondito sul machine learning quantistico offerto dall' IBM Quantum Learning\n",
        "  * Tutorial [sull'addestramento con kernel quantistico](/docs/tutorials/quantum-kernel-training)\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f082899c-b763-4df0-a81c-5efb3ca43451",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Riferimenti\n",
        "\n",
        "1. Utro, Filippo, et al. \"[Previsione migliorata della citotossicità delle cellule T CAR con metodi Quantum-Kernel](https://arxiv.org/abs/2507.22710) \" arXiv preprint arXiv:2507.22710 (2025).\n",
        "2. Huang, Hsin-Yuan, et al. \"[Il potere dei dati nell'apprendimento automatico quantistico](https://www.nature.com/articles/s41467-021-22539-9) \" Nature communications 12.1 (2021): 2631.\n",
        "3. Daniels, Kyle G., et al. \"[Decodifica del fenotipo delle cellule T CAR mediante librerie combinatorie di motivi di segnalazione e apprendimento automatico](https://www.science.org/doi/full/10.1126/science.abq0225) \" Science 378.6625 (2022): 1194-1200.\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": 2.5,
    "qpuSeconds": 4800
  },
  "nbformat": 4,
  "nbformat_minor": 4
}