{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "727f8133-a32e-4bd9-a6c3-1a1c580e06bd",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Miglioramento dei valori attesi mediante l'assorbimento del rumore propagato (PNA)\"\n",
        "description: \"Utilizzare l'assorbimento del rumore propagato (PNA) con Samplomatic e la primitiva Executor per mitigare il rumore dei gate a due qubit nella stima del valore atteso\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore pstr expvals postsel broadcastable fontsize borderaxespad reweighting */}\n",
        "\n",
        "<span id=\"improve-expectation-values-with-propagated-noise-absorption-pna\" />\n",
        "\n",
        "# Miglioramento dei valori attesi mediante l'assorbimento del rumore propagato (PNA)\n",
        "\n",
        "*Stima del tempo di esecuzione: 10 minuti su un processore Heron (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1b2c3d4-0001-4000-8000-000000000001",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Risultati di apprendimento\n",
        "\n",
        "Dopo aver seguito questo tutorial, gli utenti dovrebbero aver compreso:\n",
        "\n",
        "* Che cos’è l’assorbimento del rumore propagato (PNA) e in che modo mitiga il rumore dei gate a due qubit assorbendo i canali di rumore inversi appresi nell’osservabile misurato\n",
        "* Come utilizzare [`samplomatic`](/docs/guides/directed-execution-model#samplomatic) per racchiudere in riquadri e annotare i livelli dei circuiti in vista di rotazioni, cambi di base e iniezione di rumore\n",
        "* Come ricavare il rumore di livello con `[NoiseLearnerV3`]\\( /docs/guides/directed-execution-model#noiselearnerv3 ) e trasformarlo in un osservabile di mitigazione del rumore con [`qiskit-addon-pna`](https://github.com/Qiskit/qiskit-addon-pna)\n",
        "* Come campionare circuiti randomizzati con le classi [`Executor`](/docs/guides/get-started-with-executor) e `QuantumProgram` in `qiskit-ibm-runtime`, e combinare PNA con TREX e la post-selezione\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Prerequisiti\n",
        "\n",
        "Consigliamo agli utenti di acquisire familiarità con i seguenti argomenti prima di seguire questo tutorial:\n",
        "\n",
        "* Il flusso di lavoro [dei modelli di](/docs/guides/intro-to-patterns) Qiskit\n",
        "* Utilizzo della primitiva [\"Estimator\"](/docs/guides/get-started-with-estimator) per calcolare i valori attesi di una grandezza osservabile\n",
        "* Tecniche di mitigazione degli errori quali il “Pauli twirling” e il TREX, illustrate nella sezione [“Combinare le opzioni di mitigazione degli errori con la primitiva Estimator”](/docs/tutorials/combine-error-mitigation-techniques)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1b2c3d4-0002-4000-8000-000000000002",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Sfondo\n",
        "\n",
        "In questo tutorial mostriamo come sfruttare gli strumenti avanzati di mitigazione degli errori disponibili in Qiskit per migliorare la stima del valore atteso negli esperimenti quantistici soggetti a rumore.\n",
        "\n",
        "<span id=\"what-is-propagated-noise-absorption-pna\" />\n",
        "\n",
        "### Che cos’è l’assorbimento del rumore propagato (PNA)?\n",
        "\n",
        "*L'assorbimento del rumore propagato è una tecnica volta a mitigare gli errori dei gate, che consiste nel propagare l'osservabile attraverso il canale di rumore inverso che influenza i gate a due qubit, ottenendo così un osservabile in grado di mitigare il rumore.*\n",
        "\n",
        "Possiamo ricorrere alla mitigazione degli errori quantistici per ricavare valori attesi utili da hardware quantistico soggetto a rumore senza richiedere una tolleranza completa ai guasti. La PNA si concentra specificamente sull'assorbimento degli effetti del rumore all'interno dell'osservabile stesso, piuttosto che sulla modifica del funzionamento del circuito.\n",
        "\n",
        "Ogni gate soggetto a rumore in un circuito quantistico può essere modellato come un gate ideale seguito da un canale di rumore. Il PNA apprende o caratterizza questi canali di rumore e ne definisce le inverse. Anziché inserire le operazioni inverse nell'esecuzione hardware (cosa che in genere non è fattibile), il PNA propaga i canali di rumore inversi in avanti attraverso il circuito e li applica all'osservabile. Questo processo trasforma l'operatore osservabile $O$ in un nuovo operatore $\\tilde{O}$, in modo tale che la misurazione di $\\tilde{O}$ su un circuito soggetto a rumore fornisca lo stesso valore atteso della misurazione di $O$ su un circuito ideale, privo di rumore.\n",
        "\n",
        "Ciò può essere sintetizzato come segue:\n",
        "\n",
        "1. Modellare ogni porta soggetta a rumore $u_i$ come un'operazione ideale seguita da un canale di rumore $\\Lambda_i$.\n",
        "2. Determinare o stimare ogni $\\Lambda_i$ utilizzando strumenti di caratterizzazione del rumore.\n",
        "3. Definire e propagare le mappe inverse del rumore $\\Lambda_i^{-1}$ in avanti attraverso il circuito utilizzando le tecniche di trasferimento di Pauli.\n",
        "4. Assorbire questi inversi nell'osservabile, ottenendo così un operatore con rumore attenuato $\\tilde{O}$.\n",
        "\n",
        "Quando il rumore viene descritto come un canale di Pauli (o, più in generale, come un canale di Pauli-Lindblad sparsificato), tale propagazione può essere effettuata in modo efficiente utilizzando la propagazione di Pauli. La propagazione di Pauli fornisce un quadro di riferimento per approssimare il modo in cui i canali di rumore inversi si trasformano man mano che attraversano strati di operazioni Clifford e non Clifford, controllando al contempo la complessità computazionale.\n",
        "\n",
        "Trasferendo la mitigazione nell'ambito dell'osservabile, il PNA evita l'elevato sovraccarico di campionamento che deriverebbe altrimenti dall'inserimento di operazioni di correzione fisica nel circuito. Viene invece eseguito il circuito rumoroso originale, mentre l'osservabile viene trasformato in un nuovo operatore $\\tilde{O}$ il cui valore atteso annulla gli effetti del rumore.\n",
        "\n",
        "<span id=\"the-pna-workflow\" />\n",
        "\n",
        "### Il flusso di lavoro PNA\n",
        "\n",
        "Il processo può essere compreso attraverso le seguenti fasi concettuali. Il primo schema illustra un esperimento standard in presenza di rumore.\n",
        "\n",
        "![Esperimento rumoroso](https://quantum.cloud.ibm.com/docs/images/tutorials/propagated-noise-absorption/noisy_expt.avif)\n",
        "\n",
        "Se apprendiamo il modello di rumore, possiamo applicarne l'inverso ed eliminare il rumore.\n",
        "\n",
        "![Esperimento con riduzione del rumore](https://quantum.cloud.ibm.com/docs/images/tutorials/propagated-noise-absorption/noise_mitigated_expt.avif)\n",
        "\n",
        "Anziché implementare il canale di rumore inverso campionandolo sulla QPU, come avviene nella cancellazione probabilistica dell'errore (PEC), lo applichiamo in modo classico all'osservabile misurato utilizzando la propagazione di Pauli. L'osservabile risultante attenua efficacemente il rumore del gate appreso al momento della misurazione.\n",
        "\n",
        "![Panoramica sulla PNA](https://quantum.cloud.ibm.com/docs/images/tutorials/propagated-noise-absorption/pna_overview.avif)\n",
        "\n",
        "<span id=\"modular-error-mitigation-with-samplomatic-and-executor\" />\n",
        "\n",
        "### Mitigazione modulare degli errori con Samplomatic ed Executor\n",
        "\n",
        "L'approccio PNA si basa su un'architettura modulare di mitigazione degli errori presente in Qiskit. Questa architettura utilizza la [`samplomatic`](https://github.com/Qiskit/samplomatic) libreria insieme alle classi `QuantumProgram` e `Executor` (aggiunte a Qiskit Runtime in `qiskit-ibm-runtime`v0.47.0 ) per rendere tecniche quali l'assorbimento del rumore propagato e il \"Pauli twirling\" componibili e riutilizzabili in diversi esperimenti.\n",
        "\n",
        "Anziché incorporare la logica di mitigazione all'interno della definizione stessa del circuito, la mitigazione viene espressa in modo dichiarativo tramite `samplomatic` annotazioni e gestita a livello di programmazione tramite `Executor`, che controlla il modo in cui i circuiti randomizzati vengono generati, eseguiti e sottoposti a post-elaborazione.\n",
        "\n",
        "In questo tutorial implementiamo un [modello Qiskit](/docs/guides/intro-to-patterns) per dimostrare come il PNA possa propagare i canali di rumore di Pauli inversi e modificare di conseguenza l'osservabile, al fine di migliorare la stima dei valori attesi su QPU soggette a rumore.\n",
        "\n",
        "<span id=\"workflow-overview\" />\n",
        "\n",
        "### Panoramica del flusso di lavoro\n",
        "\n",
        "* **Fase 1: Trasposizione al problema quantistico**\n",
        "  * Costruire un modello di Ising con kick \"Trotterizzato\" speculare e un osservabile di riferimento.\n",
        "* **Fase 2: Caratterizzazione e propagazione del rumore**\n",
        "  * Utilizzare `samplomatic` per identificare e annotare gli strati e le misurazioni univoci a due qubit presenti nel circuito.\n",
        "  * Scopri quali rumori influenzano ogni singolo livello utilizzando `NoiseLearnerV3`.\n",
        "  * Associa ogni `InjectNoise` annotazione al modello di rumore appreso corrispondente.\n",
        "  * Utilizzare `qiskit-addon-pna` per propagare i canali di rumore inversi in avanti attraverso il circuito e incorporarli nell'osservabile di destinazione.\n",
        "* **Fase 3: Eseguire esperimenti quantistici**\n",
        "  * Definire un `QuantumProgram` per specificare un campionamento casuale tramite `samplex` ed eseguire gli esperimenti sul backend utilizzando il `Executor`.\n",
        "* **Fase 4: Ricostruzione e analisi dei risultati**\n",
        "  * Confrontare le strategie di mitigazione (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) e visualizzare il miglioramento rispetto ai risultati senza mitigazione.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3b64ad83-0000-4000-8000-000000000003",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisiti\n",
        "\n",
        "Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:\n",
        "\n",
        "* Qiskit SDK v2.2 o versioni successive, con supporto [alla visualizzazione](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.47 o versioni successive (`pip install qiskit-ibm-runtime`)\n",
        "* Samplomatic v0.13 o o versioni successive (`pip install samplomatic`)\n",
        "* Componente aggiuntivo PNA per Qiskit (`pip install qiskit-addon-pna`)\n",
        "* Utilità aggiuntive di Qiskit (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "28ad8a65-0000-4000-8000-000000000004",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configura\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "3a851a58-0000-4000-8000-000000000005",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit import QuantumCircuit\n",
        "from qiskit.quantum_info import Pauli, SparsePauliOp\n",
        "from qiskit.transpiler import generate_preset_pass_manager, PassManager\n",
        "from qiskit_ibm_runtime import (\n",
        "    QiskitRuntimeService,\n",
        "    QuantumProgram,\n",
        "    Executor,\n",
        "    NoiseLearnerV3,\n",
        ")\n",
        "from qiskit_addon_utils.exp_vals.measurement_bases import (\n",
        "    get_measurement_bases,\n",
        ")\n",
        "from qiskit_addon_utils.exp_vals.expectation_values import (\n",
        "    executor_expectation_values,\n",
        ")\n",
        "from qiskit_addon_utils.noise_management import trex_factors\n",
        "from qiskit_addon_utils.noise_management.post_selection import PostSelector\n",
        "from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (\n",
        "    AddPostSelectionMeasures,\n",
        "    AddSpectatorMeasures,\n",
        ")\n",
        "from qiskit_addon_pna import generate_noise_mitigating_observable\n",
        "import samplomatic\n",
        "from samplomatic.transpiler import generate_boxing_pass_manager\n",
        "from samplomatic.annotations import InjectNoise\n",
        "from samplomatic.utils import get_annotation, find_unique_box_instructions\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "\n",
        "# Selects a connected chain of low-error qubits on the target backend.\n",
        "# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line\n",
        "# of connected physical qubits; the helper walks the backend's coupling map\n",
        "# and grows a chain along the lowest-error two-qubit edges, so it works for\n",
        "# any backend rather than relying on a hardcoded layout.\n",
        "def find_qubit_chain(backend, length):\n",
        "    \"\"\"Find a connected chain of ``length`` physical qubits on ``backend``.\n",
        "\n",
        "    The chain is grown greedily along the lowest-error two-qubit edges, so it\n",
        "    favors better-performing qubits. Because the mirrored kicked-Ising circuit\n",
        "    is a 1D chain, a connected line is all we need.\n",
        "    \"\"\"\n",
        "    target = backend.target\n",
        "\n",
        "    # Identify the native two-qubit gate and build a per-edge error lookup.\n",
        "    two_qubit_gate = next(\n",
        "        name\n",
        "        for name in target.operation_names\n",
        "        if target[name]\n",
        "        and all(q is not None and len(q) == 2 for q in target[name])\n",
        "    )\n",
        "    edge_error = {\n",
        "        frozenset(qargs): (\n",
        "            1.0 if props is None or props.error is None else props.error\n",
        "        )\n",
        "        for qargs, props in target[two_qubit_gate].items()\n",
        "    }\n",
        "\n",
        "    graph = backend.coupling_map.graph.to_undirected(multigraph=False)\n",
        "    neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}\n",
        "\n",
        "    def first_chain_from(start):\n",
        "        path, visited = [start], {start}\n",
        "\n",
        "        def grow():\n",
        "            if len(path) == length:\n",
        "                return True\n",
        "            node = path[-1]\n",
        "            order = sorted(\n",
        "                neighbors[node],\n",
        "                key=lambda m: edge_error.get(frozenset((node, m)), 1.0),\n",
        "            )\n",
        "            for nxt in order:\n",
        "                if nxt not in visited:\n",
        "                    visited.add(nxt)\n",
        "                    path.append(nxt)\n",
        "                    if grow():\n",
        "                        return True\n",
        "                    path.pop()\n",
        "                    visited.remove(nxt)\n",
        "            return False\n",
        "\n",
        "        return path if grow() else None\n",
        "\n",
        "    def chain_cost(path):\n",
        "        return sum(\n",
        "            edge_error.get(frozenset((path[i], path[i + 1])), 1.0)\n",
        "            for i in range(len(path) - 1)\n",
        "        )\n",
        "\n",
        "    # Try low-degree qubits first (the natural ends of long chains) and keep\n",
        "    # the lowest-error chain found.\n",
        "    best_path, best_cost = None, float(\"inf\")\n",
        "    for start in sorted(neighbors, key=lambda n: len(neighbors[n])):\n",
        "        chain = first_chain_from(start)\n",
        "        if chain is not None and (cost := chain_cost(chain)) < best_cost:\n",
        "            best_path, best_cost = chain, cost\n",
        "\n",
        "    if best_path is None:\n",
        "        raise ValueError(\n",
        "            f\"Could not find a connected chain of {length} qubits \"\n",
        "            f\"on '{backend.name}'.\"\n",
        "        )\n",
        "    return best_path"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1b2c3d4-0006-4000-8000-000000000006",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Esempio di simulatore su piccola scala\n",
        "\n",
        "Il PNA riduce il rumore *fisico* dei gate a due qubit di uno specifico processore quantistico. Il flusso di lavoro dipende da due servizi hardware che non hanno un equivalente significativo in un simulatore ideale:\n",
        "\n",
        "* `NoiseLearnerV3` caratterizza sperimentalmente il canale di rumore sparso di Pauli-Lindblad associato a ciascun singolo strato a due qubit del circuito transpiled. Su un simulatore silenzioso non c'è alcun rumore da apprendere, e il canale inverso propagato sarebbe quello identitario.\n",
        "* La `Executor` funzione \"primitive\" campiona i circuiti intrecciati e casuali generati da `samplomatic` su un backend.\n",
        "\n",
        "In linea di principio, si potrebbe ricorrere a un modello di rumore sintetico. Ad esempio, potresti allegare `PauliLindbladError` delle istruzioni con [Qiskit Aer](https://github.com/Qiskit/qiskit-aer) e passare il circuito rumoroso risultante direttamente a `generate_noise_mitigating_observable`. Tuttavia, questo non fa altro che convalidare la contabilità classica rispetto al rumore che hai inserito tu stesso, e oscura il senso della tecnica. Per questo motivo tralasciamo l'esempio con il simulatore e illustriamo il flusso di lavoro completo del PNA direttamente sull'hardware, con ogni fase del modello Qiskit descritta in dettaglio qui di seguito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "306f6858-0000-4000-8000-000000000007",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Esempio di hardware su larga scala\n",
        "\n",
        "Ora eseguiamo il flusso di lavoro PNA completo su un modello di Ising a 30 siti con effetto \"kicked\", eseguito su un hardware d IBM Quantum®, seguendo le quattro fasi di un modello Qiskit.\n",
        "\n",
        "<span id=\"step-1-map-to-a-quantum-problem\" />\n",
        "\n",
        "### Fase 1: Trasposizione a un problema quantistico\n",
        "\n",
        "**Generare il circuito di Trotter speculare e l'osservabile**\n",
        "\n",
        "In questo esperimento studieremo la dinamica temporale di un modello di Ising a 30 siti con kick su una catena di spin di tipo “ 1D ”. L'hamiltoniano considerato è:\n",
        "\n",
        "$H = -J\\sum\\limits_{\\langle i,j \\rangle} Z_iZ_j + h\\sum\\limits_iX_i$,\n",
        "\n",
        "dove $J>0$ descrive l'accoppiamento tra gli spin più vicini, $i<j$, mentre il campo trasversale globale, $h$, è impostato su $\\frac{\\pi}{8}$. Quanto più $h$ si allontana da un angolo di Clifford (ovvero $\\theta=n\\frac{\\pi}{2}, n \\in \\mathbb{Z}$ ), tanto più difficile diventa propagare i generatori di antirumore attraverso il circuito.\n",
        "\n",
        "Per quanto riguarda la scelta dell’osservabile, considereremo la magnetizzazione media per singolo sito, $\\frac{1}{N} \\sum_{i=1}^{N} \\langle z_i \\rangle$, dove $N$ è il numero di siti.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "8fcb5af8-64f0-498a-821f-11bcd7aea203",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/8fcb5af8-64f0-498a-821f-11bcd7aea203-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 2,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "num_qubits = 30\n",
        "num_trotter_steps = 10\n",
        "rx_angle = np.pi / 8\n",
        "\n",
        "# Avg single-site magnetization\n",
        "id_pauli = Pauli(\"I\" * num_qubits)\n",
        "observable = (\n",
        "    SparsePauliOp([id_pauli.dot(Pauli(\"Z\"), [i]) for i in range(num_qubits)])\n",
        "    / num_qubits\n",
        ")\n",
        "\n",
        "# Implement Trotterized kicked-Ising model\n",
        "circuit = QuantumCircuit(num_qubits)\n",
        "for _step in range(num_trotter_steps):\n",
        "    circuit.rx(rx_angle, range(num_qubits))\n",
        "    for first_qubit in (1, 2):\n",
        "        for idx in range(first_qubit, num_qubits, 2):\n",
        "            # equivalent to Rzz(-pi/2):\n",
        "            circuit.sdg([idx - 1, idx])\n",
        "            circuit.cz(idx - 1, idx)\n",
        "# Append the inverse circuit to complete the mirroring\n",
        "circuit.compose(circuit.inverse(), inplace=True)\n",
        "circuit.measure_active()\n",
        "circuit.draw(\"mpl\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c4216992-0000-4000-8000-000000000008",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-the-problem-for-hardware-execution\" />\n",
        "\n",
        "### Fase 2: Ottimizzare il problema per l'esecuzione su hardware\n",
        "\n",
        "Il passo successivo consiste nel preparare il nostro circuito Trotter specchiato per l'esecuzione su un hardware reale IBM Quantum. L'esecuzione su una QPU non si limita alla semplice costruzione del circuito astratto, poiché è necessario ottimizzarlo in modo che:\n",
        "\n",
        "1. **Rispetta l'insieme di porte e la connettività nativi del backend.**\n",
        "   La transpilazione mappa il circuito logico in un circuito ISA supportato dal backend di destinazione. Ciò garantisce che ogni interazione tra gate e qubit sia fisicamente realizzabile.\n",
        "\n",
        "2. **Possiamo caratterizzare il rumore a livello dei livelli circuitali.**\n",
        "   Il PNA si basa sull'apprendimento e sulla propagazione dei canali di rumore inversi. Per farlo in modo efficiente, suddividiamo il circuito transpilato in livelli “incapsulati” unici da due qubit. Queste caselle ci consentono di associare a ciascun strato del circuito il proprio modello di rumore appreso.\n",
        "\n",
        "3. **Possiamo inserire modelli di rumore realistici nel PNA.**\n",
        "   Una volta che il circuito è stato incapsulato, utilizziamo il `NoiseLearnerV3` servizio per individuare sperimentalmente i canali di rumore di Pauli che influenzano ogni singolo strato a due qubit. Questi modelli appresi vengono poi ricollegati al circuito tramite le annotazioni di Samplomatic.\n",
        "\n",
        "Nel complesso, questa fase colma il divario tra un circuito speculare idealizzato e un circuito pronto per l'implementazione hardware con modelli di rumore appresi. Con questa configurazione, il PNA è in grado di propagare canali di rumore inversi attraverso il circuito e di regolare di conseguenza l'osservabile.\n",
        "\n",
        "<span id=\"connect-to-the-backend-and-transpile-to-an-isa-circuit\" />\n",
        "\n",
        "#### Connettersi al backend e traspilare in un circuito ISA\n",
        "\n",
        "Per prima cosa inizializziamo un servizio di comput IBM Quantum e e selezioniamo un backend. Effettua l'autenticazione con il tuo account seguendo le [istruzioni per salvare le tue credenziali](/docs/guides/cloud-setup); in seguito, il sistema le `QiskitRuntimeService()` rileverà automaticamente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5c6ee966-0000-4000-8000-000000000009",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Selected backend: ibm_fez\n"
          ]
        }
      ],
      "source": [
        "# Initialize IBM Quantum Compute Service using your saved credentials\n",
        "service = QiskitRuntimeService()\n",
        "\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=num_qubits\n",
        ")\n",
        "# Re-fetch with fractional gates enabled (least_busy does not forward this)\n",
        "# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.\n",
        "backend = service.backend(backend.name, use_fractional_gates=True)\n",
        "print(f\"Selected backend: {backend.name}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bd2cea49-0000-4000-8000-000000000010",
      "metadata": {},
      "source": [
        "Successivamente, scegliamo una catena connessa di qubit sul backend e trasponiamo il circuito su di essa.\n",
        "\n",
        "Utilizzando l'helper `find_qubit_chain` definito nella sezione \"Setup\", selezioniamo una linea di `num_qubits` qubit fisici collegati. `initial_layout`Eseguiamo quindi la transpilazione con `optimization_level=0` e fissiamo questa catena come, che preserva esattamente la struttura a livelli di porte a due qubit del circuito speculare. Tale struttura è alla base delle fasi di “boxing” e di “noise-learning”, pertanto in questo caso è necessario evitare un livello di ottimizzazione più elevato (che annullerebbe i gate specchiati).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "8c9afe5e-6fa3-427d-a672-becdb14490b8",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/8c9afe5e-6fa3-427d-a672-becdb14490b8-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Find a connected, low-error chain of qubits on the chosen backend\n",
        "layout = find_qubit_chain(backend, num_qubits)\n",
        "\n",
        "# Transpile the circuit for the target backend, pinning the chain as the layout.\n",
        "# optimization_level=0 preserves the mirrored two-qubit-gate layers that the\n",
        "# boxing and noise-learning steps rely on.\n",
        "pm = generate_preset_pass_manager(\n",
        "    backend=backend, optimization_level=0, initial_layout=layout\n",
        ")\n",
        "isa_circuit = pm.run(circuit)\n",
        "isa_observable = observable.apply_layout(isa_circuit.layout)\n",
        "isa_circuit.draw(\"mpl\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5d7f16f6-0000-4000-8000-000000000011",
      "metadata": {},
      "source": [
        "<span id=\"twirl-the-two-qubit-gate-layers-and-measurements-and-find-unique-layers\" />\n",
        "\n",
        "### Ruota gli strati dei gate a due qubit e delle misurazioni, e individua gli strati univoci\n",
        "\n",
        "Utilizziamo `samplomatic` per incapsulare il circuito e identificare livelli univoci a due qubit. Un box è un costrutto che raggruppa una serie di istruzioni in modo che determinati intenti o annotazioni possano essere successivamente applicati in modo uniforme a tutti i gate all’interno dello stesso box.\n",
        "\n",
        "In questo caso, chiamiamo il metodo `generate_boxing_pass_manager`, che va oltre la semplice identificazione di strati a due qubit. Svolge diverse funzioni fondamentali:\n",
        "\n",
        "* Raggruppa tutti gli strati a due qubit presenti nel circuito,\n",
        "* Applica le `Twirl` annotazioni e `ChangeBasis` a quei livelli,\n",
        "* raggruppa le operazioni di misurazione in sezioni a sé stanti, e\n",
        "* Applica l'annotazione `InjectNoise` a ciascun strato di due qubit.\n",
        "\n",
        "Queste annotazioni definiscono come vengono gestiti il rumore, i cambiamenti di base e il twirling lungo tutto il circuito. Inoltre, definiscono la struttura che verrà utilizzata in seguito per lo studio e la riduzione del rumore.\n",
        "\n",
        "Le opzioni di configurazione principali sono:\n",
        "\n",
        "* `enable_gates`/`enable_measures: True`: Inserire in un riquadro tutti gli strati di gate a due qubit e le misurazioni terminali. All'interno delle caselle, le porte a singolo qubit sono rappresentate con orientamento a sinistra.\n",
        "* `measure_annotations: all`: Includere `Twirl` le annotazioni e `ChangeBasis` sulla casella di misura.\n",
        "* `twirling_strategy: active`: Ruota tutti i qubit attivi in ogni casella contenente porte di entanglement.\n",
        "* `Twirl``inject_noise_targets: gates`: Aggiungi `InjectNoise` annotazioni a tutte le caselle annotate contenenti porte di entanglement.\n",
        "* `inject_noise_strategy: uniform_modification`: Regolare in modo uniforme tutti i livelli di rumore in tutto il circuito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "d4364c0b-2ddc-473d-8171-fd39b30a1e2d",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Box up circuit with Twirl and InjectNoise annotations\n",
        "pm = generate_boxing_pass_manager(\n",
        "    enable_gates=True,\n",
        "    enable_measures=True,\n",
        "    measure_annotations=\"all\",\n",
        "    twirling_strategy=\"active\",\n",
        "    inject_noise_targets=\"gates\",\n",
        "    inject_noise_strategy=\"uniform_modification\",\n",
        ")\n",
        "boxed_circuit = pm.run(isa_circuit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "9c911a29-553c-4ffe-94de-1442f25e3116",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/9c911a29-553c-4ffe-94de-1442f25e3116-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "draw_circ = QuantumCircuit(boxed_circuit.num_qubits)\n",
        "draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)\n",
        "draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)\n",
        "draw_circ.draw(\"mpl\", fold=-1, scale=0.3, idle_wires=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cc283451-0000-4000-8000-000000000014",
      "metadata": {},
      "source": [
        "Generare il circuito modello e `samplex`, che definiscono le modalità di campionamento del circuito.\n",
        "\n",
        "Qui aggiungiamo anche le misurazioni dello spettatore e quelle post-selezione, necessarie per eseguire la post-selezione sui campioni generati dal `Executor`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "a0ca9fbb-c572-4d49-bdef-e056c1bc82e3",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Build template circuit and samplex for later use with the \"Executor\"\n",
        "template_circuit, samplex = samplomatic.build(boxed_circuit)\n",
        "\n",
        "# Add post-selection instructions to the template circuit\n",
        "post_selection_pm = PassManager(\n",
        "    [\n",
        "        AddSpectatorMeasures(backend.coupling_map),\n",
        "        AddPostSelectionMeasures(x_pulse_type=\"rx\"),\n",
        "    ]\n",
        ")\n",
        "template_circuit = post_selection_pm.run(template_circuit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "8d46f7b1-f0e9-4bc3-ba0f-5cc96090a945",
      "metadata": {
        "jupyter": {
          "source_hidden": true
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/8d46f7b1-f0e9-4bc3-ba0f-5cc96090a945-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 8,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "draw_circ = template_circuit.copy_empty_like()\n",
        "draw_circ.data = template_circuit.data[:324]\n",
        "draw_circ.draw(\"mpl\", fold=-1, scale=0.3, idle_wires=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a45820c6-0000-4000-8000-000000000015",
      "metadata": {},
      "source": [
        "<span id=\"learn-the-noise-using-noiselearnerv3\" />\n",
        "\n",
        "### Impara a riconoscere il rumore utilizzando `NoiseLearnerV3`\n",
        "\n",
        "Prima di poter applicare il PNA per la mitigazione degli errori, dobbiamo innanzitutto caratterizzare il rumore che agisce su ciascuno dei singoli livelli a due qubit e sul livello di misurazione del nostro circuito. A tal fine, utilizziamo il `NoiseLearnerV3` programma per apprendere sperimentalmente i modelli di rumore per ciascun strato identificato in precedenza. Il sistema di apprendimento esegue esperimenti di tipo benchmarking che stimano il rumore del canale che influenza ciascun strato e restituisce un oggetto risultato contenente il modello appreso.\n",
        "\n",
        "Iniziamo identificando gli strati univoci del nostro circuito utilizzando `find_unique_box_instructions` da `samplomatic`. In questo modo ci assicuriamo di apprendere il rumore una sola volta per ogni tipo di strato distinto, riducendo al minimo il numero di esperimenti e il costo totale degli shot. L'elenco dei livelli così ottenuto viene trasmesso al modello di apprendimento del rumore.\n",
        "\n",
        "Esistono alcuni parametri chiave che determinano il modo in cui viene appreso il rumore:\n",
        "\n",
        "* `num_randomizations`: Numero di circuiti casuali utilizzati per ciascuna configurazione di apprendimento.\n",
        "* `shots_per_randomization`: Numero di tentativi effettuati per ogni ciclo di apprendimento casuale.\n",
        "* `layer_pair_depths`: Le profondità del circuito (misurate in numero di coppie) da utilizzare negli esperimenti di apprendimento.\n",
        "* `post_selection`: Consente la post-selezione basata sui fronti utilizzando `rx` i gate per applicare impulsi post-misura.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "24670819-0000-4000-8000-000000000016",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Noise learning parameters\n",
        "num_randomizations_nl = 64\n",
        "shots_per_randomization_nl = 128\n",
        "\n",
        "# Match the deepest noise-learning depth to the depth of the circuit being\n",
        "# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its\n",
        "# inverse), so the deepest value is half the circuits's two-qubit-layer depth.\n",
        "# Learning to this depth improves the quality of the mitigation.\n",
        "\n",
        "depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)\n",
        "max_layer_pair_depth = depth_2q // 2\n",
        "\n",
        "# Use a fixed schedule of learning depths, but drop any that exceed the circuit's\n",
        "# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never\n",
        "# learn deeper than the circuit being mitigated\n",
        "candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]\n",
        "layer_pair_depths = sorted(\n",
        "    {d for d in candidate_depths if d < max_layer_pair_depth}\n",
        "    | {max_layer_pair_depth}\n",
        ")\n",
        "\n",
        "# Find the unique instructions (layers) from the boxed-up circuit\n",
        "unique_2q_layers_and_meas = find_unique_box_instructions(\n",
        "    boxed_circuit, normalize_annotations=None, undress_boxes=True\n",
        ")\n",
        "\n",
        "# Configure and run the noise learner on the unique layers.\n",
        "# Options can be passed directly as a dictionary.\n",
        "noise_learner_options = {\n",
        "    \"num_randomizations\": num_randomizations_nl,\n",
        "    \"shots_per_randomization\": shots_per_randomization_nl,\n",
        "    \"layer_pair_depths\": layer_pair_depths,\n",
        "    \"post_selection\": {\n",
        "        \"enable\": True,\n",
        "        \"strategy\": \"edge\",\n",
        "        \"x_pulse_type\": \"rx\",\n",
        "    },\n",
        "    \"environment\": {\"job_tags\": [\"TUT_PNA\"]},\n",
        "}\n",
        "\n",
        "noise_learner = NoiseLearnerV3(backend, noise_learner_options)\n",
        "noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)\n",
        "noise_learner_result = noise_learner_job.result()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "aa248719-0000-4000-8000-000000000017",
      "metadata": {},
      "source": [
        "<span id=\"visualize-the-learned-noise-rates\" />\n",
        "\n",
        "### Visualizza i tassi di rumore appresi\n",
        "\n",
        "Dopo aver appreso i modelli di rumore, possiamo esaminare la distribuzione dei tassi di errore dedotti sia per le operazioni su un qubit che per quelle su due qubit. Il codice riportato di seguito estrae le rappresentazioni di Pauli-Lindblad dai risultati relativi al rumore appreso e raccoglie i corrispondenti tassi di rumore.\n",
        "\n",
        "Per ogni strato addestrato:\n",
        "\n",
        "* Convertiamo il modello di rumore in un elenco sparso di `(pstr, qubits, rate)` tuple, dove `pstr` è la stringa di Pauli che agisce sui qubit dati e `rate` è il tasso di errore associato.\n",
        "* Suddividiamo le frequenze in termini relativi a un qubit (`len(pstr) == 1`) e a due qubit (`len(pstr) == 2`).\n",
        "* Gli elenchi delle tariffe vengono quindi ordinati e se ne calcolano i valori mediani.\n",
        "\n",
        "Rappresentiamo graficamente le distribuzioni dei tassi di rumore per un qubit (rosso) e due qubit (blu) su una scala logaritmica, con i rispettivi valori mediani indicati da linee verticali, in modo da poter confrontare le grandezze relative dei generatori di Pauli-Lindblad appresi. L'ordine dei tassi relativi ai qubit singoli e ai qubit doppi dipende dal dispositivo e dagli strati specifici sottoposti a caratterizzazione; in questa serie di esperimenti, i generatori a qubit singolo ( weight-1 ) presentano il tasso mediano più elevato.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "b1a8ad48-2875-41e0-92c4-c28b34083e6b",
      "metadata": {
        "jupyter": {
          "source_hidden": true
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<matplotlib.legend.Legend at 0x125336120>"
            ]
          },
          "execution_count": 10,
          "metadata": {},
          "output_type": "execute_result"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/b1a8ad48-2875-41e0-92c4-c28b34083e6b-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "hw_rates_1q = []\n",
        "hw_rates_2q = []\n",
        "for nlr in noise_learner_result[:2]:\n",
        "    plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()\n",
        "    hw_rates_1q += [\n",
        "        rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1\n",
        "    ]\n",
        "    hw_rates_2q += [\n",
        "        rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2\n",
        "    ]\n",
        "hw_rates_1q = sorted(hw_rates_1q)\n",
        "hw_rates_2q = sorted(hw_rates_2q)\n",
        "median_1q = hw_rates_1q[len(hw_rates_1q) // 2]\n",
        "median_2q = hw_rates_2q[len(hw_rates_2q) // 2]\n",
        "fig, ax = plt.subplots(1, 1, figsize=(14, 5))\n",
        "ax.scatter(\n",
        "    (hw_rates_1q),\n",
        "    [(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],\n",
        "    color=\"red\",\n",
        "    label=\"1q rates\",\n",
        ")\n",
        "ax.set_xscale(\"log\")\n",
        "ax.set_ylim(0, 1.1)\n",
        "ax.vlines(median_1q, 0, 1, color=\"red\")\n",
        "ax.text(median_1q * 1.1, 0.1, f\"{median_1q:.2e}\")\n",
        "ax.scatter(\n",
        "    (hw_rates_2q),\n",
        "    [(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],\n",
        "    color=\"blue\",\n",
        "    label=\"2q rates\",\n",
        ")\n",
        "ax.set_xscale(\"log\")\n",
        "ax.set_ylim(0, 1.1)\n",
        "ax.vlines(median_2q, 0, 1, color=\"blue\")\n",
        "ax.text(median_2q * 1.1, 0.2, f\"{median_2q:.2e}\")\n",
        "ax.set_title(\"Learned noise rates\")\n",
        "ax.set_xlabel(\"Noise rate\")\n",
        "ax.set_yticks([])\n",
        "plt.legend()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f8a91705-0000-4000-8000-000000000018",
      "metadata": {},
      "source": [
        "<span id=\"associate-circuit-boxes-with-learned-noise\" />\n",
        "\n",
        "### Associare le scatole di derivazione al rumore appreso\n",
        "\n",
        "Una volta ottenuti i modelli di rumore per ogni singolo strato a due qubit, dobbiamo collegarli alle corrispondenti `InjectNoise` annotazioni all’interno del circuito racchiuso nel riquadro.\n",
        "\n",
        "La `InjectNoise` direttiva è un'annotazione `samplomatic` che utilizza i \"dresser\" a singolo qubit per iniettare rumore nel circuito in modo controllato e configurabile. Consente una modellizzazione modulare del rumore su diversi livelli.\n",
        "\n",
        "Ogni `InjectNoise` annotazione comprende:\n",
        "\n",
        "* `InjectNoise.ref` - un identificatore univoco per l'annotazione. Viene utilizzato dall'oggetto `samplex` per assegnare correttamente il modello di rumore corrispondente.\n",
        "* `InjectNoise.modifier_ref` *(facoltativo)* - un riferimento secondario che consente di ridimensionare il modello di rumore assegnato mediante un fattore moltiplicativo.\n",
        "\n",
        "In questa fase, definiamo una corrispondenza tra ciascun `InjectNoise.ref` e il relativo modello di rumore appreso (`PauliLindbladMap`). Tale corrispondenza garantisce che ogni strato di gate di entanglement nel circuito sia abbinato al modello di rumore appropriato, in modo che gli effetti del rumore vengano applicati con precisione durante il campionamento e le successive fasi di mitigazione del rumore.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "e172d227-7a04-48ba-8566-d5f3007ef7bc",
      "metadata": {},
      "outputs": [],
      "source": [
        "# map inject noise refs to pauli lindblad maps\n",
        "refs_to_noise_models = {}\n",
        "for instruction, result in zip(\n",
        "    unique_2q_layers_and_meas, noise_learner_result, strict=False\n",
        "):\n",
        "    if inject_noise_annot := get_annotation(\n",
        "        instruction.operation, InjectNoise\n",
        "    ):\n",
        "        refs_to_noise_models[inject_noise_annot.ref] = (\n",
        "            result.to_pauli_lindblad_map()\n",
        "        )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7ab22f68-0000-4000-8000-000000000019",
      "metadata": {},
      "source": [
        "<span id=\"propagate-the-observable-through-the-learned-anti-noise\" />\n",
        "\n",
        "### Propagare l'osservabile attraverso l'anti-rumore appreso\n",
        "\n",
        "Come già detto, questa operazione si svolge in due fasi. Per prima cosa, estendiamo un generatore di antirumore fino alla fine del circuito. Successivamente, propaghiamo l’osservabile attraverso quel generatore evoluto. Questo processo viene ripetuto per ciascun generatore anti-rumore presente nel circuito. In questa implementazione, ogni generatore di un determinato livello viene propagato in parallelo fino alla fine del circuito. Inoltre, si ricorre al multiprocessing Python per eseguire in parallelo sia la propagazione in avanti dell’anti-rumore sia la propagazione all’indietro dell’osservabile. Ciò impedisce l'accumulo di generatori evoluti in memoria e ottimizza al massimo le risorse di calcolo.\n",
        "\n",
        "Quando si esegue PNA, è sempre necessario specificare un circuito soggetto a rumore e una grandezza osservabile. Se il circuito rumoroso è un circuito racchiuso in un riquadro con `InjectNoise` delle annotazioni, è necessario fornire la mappatura che abbiamo creato nel passaggio precedente. È anche possibile passare un circuito non boxed contenente `PauliLindbladError` istruzioni provenienti da `qiskit-aer`. In tal caso, `refs_to_noise_models` non è necessario fornirlo. Oltre agli input principali, si prega di tenere in considerazione anche quanto segue:\n",
        "\n",
        "* `max_err_terms`: Il numero di termini da mantenere in ciascun generatore anti-rumore man mano che la propagazione procede in avanti. Aumentare questo valore migliora in genere la precisione, ma non è garantito che tale andamento sia monotono.\n",
        "* `max_obs_terms`: Il numero di termini da mantenere nell'osservabile di mitigazione del rumore, $\\tilde{O}$, man mano che viene propagato a ritroso attraverso l'anti-rumore evoluto. Valori più elevati aumentano generalmente la precisione, ma non è garantito che ciò avvenga in modo monotono.\n",
        "* `num_processes`: Il numero di core da dedicare al processo. Ricordate che i generatori vengono propagati in avanti e applicati all'osservabile in parallelo.\n",
        "* `search_step`: La fase di retropropagazione utilizza un metodo avido per coniugare approssimativamente due operatori nella base di Pauli. È possibile velocizzare questo metodo aumentando `search_step`. Vedi il [`pauli-prop` Consultare la documentazione](https://qiskit.github.io/pauli-prop/) per ulteriori informazioni.\n",
        "* `num_to_measure`: Sebbene questa variabile non sia un parametro di input per `generate_noise_mitigating_observable`, la utilizziamo per controllare quanti termini di $\\tilde{O}$ vogliamo effettivamente misurare. In questo caso prendiamo in considerazione solo i primi 30 termini, che corrispondono ai termini originali presenti nel nostro osservabile. I termini sono stati ora ridimensionati in modo tale che la loro misurazione abbia l'effetto di attenuare il rumore del gate appreso. Sebbene si misurino solo 30 termini da $\\tilde{O}$, spesso è comunque utile lasciare che il numero cresca, poiché ciò aumenta la precisione dei fattori di scala dei termini principali.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "19407dc3-eec0-4323-9a8a-47e949fb6ae2",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Finished! 13740 / 13740 generators propagated.                       "
          ]
        }
      ],
      "source": [
        "# PNA parameters\n",
        "num_processes = 8\n",
        "max_err_terms = 10_000\n",
        "max_obs_terms = 10_000\n",
        "num_to_measure = num_qubits\n",
        "\n",
        "obs_tilde_isa = generate_noise_mitigating_observable(\n",
        "    boxed_circuit,\n",
        "    isa_observable,\n",
        "    refs_to_noise_models,\n",
        "    max_err_terms=max_err_terms,\n",
        "    max_obs_terms=max_obs_terms,\n",
        "    num_processes=num_processes,\n",
        "    print_progress=True,\n",
        "    search_step=8,\n",
        ")\n",
        "p_2_v = {p: v for v, p in enumerate(layout)}\n",
        "obs_tilde_virtual = SparsePauliOp.from_sparse_list(\n",
        "    [\n",
        "        (pstr, [p_2_v[p] for p in p_qubits], coeff)\n",
        "        for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()\n",
        "    ],\n",
        "    num_qubits=num_qubits,\n",
        ")\n",
        "obs_tilde_virtual = obs_tilde_virtual[\n",
        "    np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]\n",
        "][:num_to_measure]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "77246aef-1892-4a27-bbf0-128b12206581",
      "metadata": {
        "jupyter": {
          "source_hidden": true
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "[<matplotlib.lines.Line2D at 0x124b0a120>]"
            ]
          },
          "execution_count": 13,
          "metadata": {},
          "output_type": "execute_result"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/77246aef-1892-4a27-bbf0-128b12206581-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]\n",
        "plt.xscale(\"log\")\n",
        "plt.yscale(\"log\")\n",
        "plt.title(r\"$\\tilde{O}$ coeff magnitudes\")\n",
        "plt.ylabel(\"Magnitude\")\n",
        "plt.xlabel(\"Pauli term index\")\n",
        "plt.plot(np.abs(obs_tilde_isa.coeffs), \".\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e02700bb-0000-4000-8000-000000000020",
      "metadata": {},
      "source": [
        "<span id=\"transform-the-measurement-bases-to-canonical-form\" />\n",
        "\n",
        "### Trasformare le basi di misura in forma canonica\n",
        "\n",
        "Successivamente, individuiamo un insieme minimo di basi di misura tale da coprire interamente ogni termine di Pauli nell'osservabile misurato ( *è possibile misurare contemporaneamente più osservabili, purché commutino a livello di qubit* ). Poiché stiamo misurando solo i termini della nostra grandezza osservabile originale, che è la somma di tutti i Pauli singoli`Z` , è necessaria una sola base: la base \"all-`Z` \".\n",
        "\n",
        "Oltre a individuare un insieme di basi di misura di Pauli, dobbiamo mappare questi termini di Pauli nella forma canonica prevista dalla `Executor`. Per ulteriori informazioni sull'ordinamento canonico dei qubit, consulta la [documentazione di Samplomatic](https://qiskit.github.io/samplomatic/guides/samplex_io.html#qubit-ordering-convention).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "a0ff4edd-434a-4aa5-8175-075e1472c81e",
      "metadata": {},
      "outputs": [],
      "source": [
        "meas_box = boxed_circuit.data[-1]\n",
        "canonical_qubits = [\n",
        "    idx\n",
        "    for idx, qubit in enumerate(boxed_circuit.qubits)\n",
        "    if qubit in meas_box.qubits\n",
        "]\n",
        "c_2_p = {\n",
        "    c: p for c, p in enumerate(canonical_qubits)\n",
        "}  # canonical -> physical\n",
        "p_2_v = {p: v for v, p in enumerate(layout)}  # physical -> virtual\n",
        "c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}  # canonical -> virtual\n",
        "meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)\n",
        "meas_bases_canonical = [\n",
        "    np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)\n",
        "    for base in meas_bases\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "24c95aca-0000-4000-8000-000000000021",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-quantum-experiments\" />\n",
        "\n",
        "### Fase 3: Eseguire esperimenti quantistici\n",
        "\n",
        "<span id=\"specify-how-to-sample-in-the-quantumprogram\" />\n",
        "\n",
        "#### Specificare le modalità di campionamento nel `QuantumProgram`\n",
        "\n",
        "Ora configuriamo il `QuantumProgram`, che funge da contenitore centrale per tutti i circuiti e le configurazioni di campionamento che verranno eseguiti dal `Executor`. Questo oggetto definisce le modalità con cui le istanze di circuito generate in modo casuale vengono create, raggruppate in batch ed eseguite per produrre i risultati di misurazione utilizzati in PNA.\n",
        "\n",
        "Un `QuantumProgram` può contenere più elementi, ciascuno dei quali è costituito da un circuito modello e da un oggetto corrispondente `samplex` che definisce le modalità di applicazione delle randomizzazioni. Questa astrazione consente al `Executor` di gestire l'intero flusso di lavoro come un unico programma modulare — dalla generazione casuale dei circuiti alla raccolta e all'aggregazione degli scatti.\n",
        "\n",
        "In questa fase, creiamo un programma `QuantumProgram` che esegue il nostro esperimento PNA utilizzando il circuito modello e `samplex` il codice che abbiamo realizzato in precedenza. La configurazione comprende i seguenti elementi:\n",
        "\n",
        "* `template_circuit`: Il circuito contenente tutte le porte logiche necessarie per implementare tutte le randomizzazioni desiderate (tra cui randomizzazioni di tipo “twirling”, parametri e così via).\n",
        "* `samplex`: Un oggetto che definisce una distribuzione di probabilità su tutte le possibili randomizzazioni del circuito da cui effettuare il campionamento.\n",
        "* `samplex_arguments`: I vincoli necessari per definire completamente il `samplex`\n",
        "  * `basis_changes`: È qui che specifichiamo un insieme di basi di misura che copra tutti i termini di Pauli presenti nell'osservabile misurato.\n",
        "  * `noise_scales.ref`: Abbiamo impostato la scala di ciascun livello di rumore su `0.0` per evitare che venga introdotto ulteriore rumore nei nostri campioni.\n",
        "  * `pauli_lindblad_maps`: Obbligatorio se `noise_scales` vengono passati. Questo non fa altro che associare i livelli di rumore al modello di rumore corrispondente.\n",
        "* `shape`: Una tupla di forma per estendere la forma implicita definita da `samplex_arguments`. Gli assi non banali introdotti da questa estensione enumerano le randomizzazioni.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "7958cdd5-37b9-48ab-a1b1-5bf2a6bcf13e",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Control the # of shots during execution\n",
        "shots_per_randomization_exec = 64\n",
        "num_randomizations_exec = 6144\n",
        "\n",
        "# Zero out the noise to prevent noise from being injected during execution.\n",
        "# We only added InjectNoise annotations so PNA could associate the noise\n",
        "# to layers in the circuit\n",
        "samplex_inputs = {f\"noise_scales.{ref}\": 0.0 for ref in refs_to_noise_models}\n",
        "samplex_inputs |= {\"pauli_lindblad_maps\": refs_to_noise_models}\n",
        "\n",
        "# Specify the bases to measure. The samplex exposes one basis-change input per\n",
        "# ChangeBasis-annotated box; here a single all-Z basis covers every term. We\n",
        "# look up the basis-change interface name rather than hardcoding an index, since\n",
        "# the name depends on the circuit's box structure.\n",
        "bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)\n",
        "samplex_inputs |= {\n",
        "    spec.name: bases_broadcastable\n",
        "    for spec in samplex.inputs().get_specs(r\"^basis_changes\\.\")\n",
        "}\n",
        "\n",
        "# Convert samplex_inputs into a dict to pass to QuantumProgram\n",
        "samplex_arguments = (\n",
        "    samplex.inputs().make_broadcastable().bind(**samplex_inputs)\n",
        ")\n",
        "\n",
        "# Instantiate the QuantumProgram with the specified parameters\n",
        "program = QuantumProgram(shots=shots_per_randomization_exec)\n",
        "program.append_samplex_item(\n",
        "    circuit=template_circuit,\n",
        "    samplex=samplex,\n",
        "    samplex_arguments=samplex_arguments,\n",
        "    shape=(num_randomizations_exec,),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b7caba08-0000-4000-8000-000000000022",
      "metadata": {},
      "source": [
        "<span id=\"sample-the-circuit-using-the-executor\" />\n",
        "\n",
        "#### Provare il circuito utilizzando il `Executor`\n",
        "\n",
        "Ora che abbiamo definito il nostro `QuantumProgram`, eseguire l'esperimento è semplicissimo. Basta creare un'istanza `Executor` dell'oggetto, fornirgli il backend ed eseguire il programma.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "44202d02-0000-4000-8000-000000000023",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Execute (sample) the circuit\n",
        "executor = Executor(backend)\n",
        "job_exec = executor.run(program)\n",
        "exec_results = job_exec.result()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bb61dcec-0000-4000-8000-000000000024",
      "metadata": {},
      "source": [
        "<span id=\"step-4-reconstruct-and-analyze-results\" />\n",
        "\n",
        "### Fase 4: Ricostruzione e analisi dei risultati\n",
        "\n",
        "Per calcolare un valore atteso corretto per l'errore, procediamo come segue:\n",
        "\n",
        "* Calcolare i fattori di scala TREX in base al rumore appreso che influenza le misurazioni,\n",
        "* Generare una maschera per mantenere solo i campioni selezionati in seguito, e\n",
        "* Utilizzare la `executor_expectation_values` funzione da `qiskit-addon-utils` per combinare tutti i dati in un valore atteso con errore ridotto.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "1ba3b057-a809-402d-8fd6-4314956dc98e",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Computing the TREX factors\n",
        "measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()\n",
        "trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)\n",
        "\n",
        "# Post-select the results\n",
        "post_selector = PostSelector.from_circuit(\n",
        "    circuit=template_circuit, coupling_map=backend.coupling_map\n",
        ")\n",
        "\n",
        "# Compute the ps mask for filtering results\n",
        "mask = post_selector.compute_mask(exec_results[0], strategy=\"edge\")\n",
        "\n",
        "# Compute expvals using post selected results\n",
        "results = executor_expectation_values(\n",
        "    exec_results[0][\"meas\"],\n",
        "    bases_reverser,\n",
        "    meas_basis_axis=0,\n",
        "    avg_axis=1,\n",
        "    measurement_flips=exec_results[0][\"measurement_flips.meas\"],\n",
        "    pauli_signs=exec_results[0].get(\"pauli_signs\", None),\n",
        "    postselect_mask=mask,\n",
        "    rescale_factors=trex_rescale_factors,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "aed4c6f9-0000-4000-8000-000000000025",
      "metadata": {},
      "source": [
        "<span id=\"compare-mitigation-strategies-pna-pna+trex-pna+ps-pna+ps+trex\" />\n",
        "\n",
        "#### Confronto tra le strategie di mitigazione: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX\n",
        "\n",
        "Calcoliamo e visualizziamo i valori attesi per diverse varianti di mitigazione utilizzando i `Executor` risultati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "8df9366c-d456-416b-80ab-3b941a07a935",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/propagated-noise-absorption/extracted-outputs/8df9366c-d456-416b-80ab-3b941a07a935-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "bases_reverser_unmit = {Pauli(\"Z\" * num_qubits): [observable]}\n",
        "args = [\n",
        "    (bases_reverser_unmit, None, None),\n",
        "    (bases_reverser, None, None),\n",
        "    (bases_reverser, None, trex_rescale_factors),\n",
        "    (bases_reverser, mask, None),\n",
        "    (bases_reverser, mask, trex_rescale_factors),\n",
        "]\n",
        "\n",
        "evs = []\n",
        "for reverser, postsel_mask, factors in args:\n",
        "    # Compute expvals using post selected results\n",
        "    res_ps = executor_expectation_values(\n",
        "        exec_results[0][\"meas\"],\n",
        "        reverser,\n",
        "        meas_basis_axis=0,\n",
        "        avg_axis=1,\n",
        "        measurement_flips=exec_results[0][\"measurement_flips.meas\"],\n",
        "        pauli_signs=exec_results[0].get(\"pauli_signs\", None),\n",
        "        postselect_mask=postsel_mask,\n",
        "        rescale_factors=factors,\n",
        "    )\n",
        "    res_ps = np.array(res_ps)\n",
        "    evs.append(res_ps[:, 0][0])\n",
        "\n",
        "experiments = [\"PNA\", \"PNA+TREX\", \"PNA+PS\", \"PNA+PS+TREX\"]\n",
        "colors = [\"#d9d9d9\", \"#b0b0b0\", \"#7f7f7f\", \"#4c4c4c\"]\n",
        "plt.bar(experiments, evs[1:], color=colors)\n",
        "plt.axhline(y=1, color=\"green\", linestyle=\"--\", linewidth=2, label=\"Ideal\")\n",
        "plt.axhline(\n",
        "    y=evs[0], color=\"red\", linestyle=\"--\", linewidth=2, label=\"Unmitigated\"\n",
        ")\n",
        "plt.ylabel(\"Expectation value\", fontsize=14)\n",
        "\n",
        "plt.title(\n",
        "    r\"30q Mirrored Ising, 10 Trotter steps, $\\theta_{rx}=\\frac{\\pi}{8}$\",\n",
        "    fontsize=14,\n",
        ")\n",
        "plt.legend(loc=\"upper left\", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)\n",
        "plt.xticks(rotation=45)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9589eae8-0000-4000-8000-000000000026",
      "metadata": {},
      "source": [
        "I risultati dimostrano i vantaggi cumulativi derivanti dalla combinazione di diverse tecniche di mitigazione degli errori. Il semplice approccio PNA riporta già il valore atteso vicino al benchmark ideale, indicando che la propagazione dei canali di rumore inversi nell’osservabile compensa efficacemente gli errori dei gate a due qubit.\n",
        "\n",
        "* L'aggiunta della riponderazione TREX (PNA+TREX) migliora leggermente la stima, correggendo lo squilibrio di campionamento nei circuiti randomizzati.\n",
        "* La post-selezione (PNA+PS) offre un miglioramento più evidente, filtrando i risultati di misurazione incoerenti che probabilmente derivano da errori residui.\n",
        "* Infine, la combinazione di tutte e tre (PNA+PS+TREX) produce il risultato più accurato, che si avvicina quasi al valore ideale, dimostrando come queste strategie di mitigazione si rafforzino a vicenda.\n",
        "\n",
        "Nel complesso, il confronto evidenzia che il PNA costituisce una solida base per la stima del valore atteso che tiene conto del rumore, mentre il TREX e la post-selezione offrono perfezionamenti complementari per ottenere ulteriori miglioramenti in termini di accuratezza.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1b2c3d4-9999-4000-8000-000000000099",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Passi successivi\n",
        "\n",
        "Se questo lavoro ti è sembrato interessante, potrebbero interessarti i seguenti contenuti:\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Suggerimenti\">\n",
        "  * [Combinare le opzioni di mitigazione degli errori con la primitiva Stimatore](/docs/tutorials/combine-error-mitigation-techniques)\n",
        "  * [Attenuazione degli errori su scala industriale mediante amplificazione probabilistica degli errori](/docs/tutorials/probabilistic-error-amplification)\n",
        "  * [Backpropagation dell'operatore (OBP) per la stima dei valori attesi](/docs/tutorials/operator-back-propagation)\n",
        "  * La [`samplomatic`](https://github.com/Qiskit/samplomatic) documentazione e [`pauli-prop`](https://github.com/Qiskit/pauli-prop) e\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    },
    "hours": 3,
    "qpuSeconds": 600
  },
  "nbformat": 4,
  "nbformat_minor": 5
}