{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "727f8133-a32e-4bd9-a6c3-1a1c580e06bd",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Mejorar los valores esperados mediante la absorción de ruido propagado (PNA)\"\n",
        "description: \"Utilizar la absorción de ruido propagado (PNA) con Samplomatic y la primitiva «Executor» para mitigar el ruido de las puertas de dos qubits en la estimación del valor esperado\"\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",
        "# Mejorar los valores esperados mediante la absorción de ruido propagado (PNA)\n",
        "\n",
        "*Tiempo de ejecución estimado: 10 minutos en un procesador Heron (NOTA: Se trata únicamente de una estimación. (El tiempo de ejecución puede variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1b2c3d4-0001-4000-8000-000000000001",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados del aprendizaje\n",
        "\n",
        "Una vez completado este tutorial, los usuarios deberían comprender:\n",
        "\n",
        "* En qué consiste la absorción de ruido propagado (PNA) y cómo mitiga el ruido de las puertas de dos qubits al absorber los canales de ruido inversos aprendidos en el observable medido\n",
        "* Cómo utilizar [`samplomatic`](/docs/guides/directed-execution-model#samplomatic) para enmarcar y anotar capas de circuitos con el fin de realizar giros, cambios de base e inyección de ruido\n",
        "* Cómo aprender el ruido de capa con `[NoiseLearnerV3`]\\( /docs/guides/directed-execution-model#noiselearnerv3 ) y propagarlo a un observable que mitigue el ruido mediante [`qiskit-addon-pna`](https://github.com/Qiskit/qiskit-addon-pna)\n",
        "* Cómo obtener muestras de circuitos aleatorios con las `QuantumProgram` clases y [`Executor`](/docs/guides/get-started-with-executor) en « Qiskit Runtime », y combinar PNA con TREX y la selección posterior\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Requisitos previos\n",
        "\n",
        "Recomendamos a los usuarios que se familiaricen con los siguientes temas antes de seguir este tutorial:\n",
        "\n",
        "* El flujo de trabajo de [Qiskit Patterns](/docs/guides/intro-to-patterns)\n",
        "* Uso de la primitiva [«Estimator»](/docs/guides/get-started-with-estimator) para calcular los valores esperados de una variable observable\n",
        "* Técnicas de mitigación de errores como el «Pauli twirling» y TREX, que se tratan en [«Combinar opciones de mitigación de errores 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",
        "## En segundo plano\n",
        "\n",
        "En este tutorial, mostramos cómo aprovechar las herramientas avanzadas de mitigación de errores de Qiskit para mejorar la estimación del valor esperado en experimentos cuánticos con ruido.\n",
        "\n",
        "<span id=\"what-is-propagated-noise-absorption-pna\" />\n",
        "\n",
        "### ¿Qué es la absorción del ruido propagado (PNA)?\n",
        "\n",
        "*La absorción de ruido propagada es una técnica para mitigar los errores de las puertas cuánticas mediante la propagación de la observable a través del canal de ruido inverso que afecta a las puertas de dos qubits, lo que da lugar a una observable que mitiga el ruido.*\n",
        "\n",
        "Podemos utilizar la mitigación de errores cuánticos para extraer valores esperados útiles a partir de hardware cuántico con ruido sin necesidad de una tolerancia total a los fallos. La PNA se centra específicamente en absorber los efectos del ruido en el propio observable, en lugar de modificar el funcionamiento del circuito.\n",
        "\n",
        "Cada puerta con ruido de un circuito cuántico puede modelarse como una puerta ideal seguida de un canal de ruido. La PNA aprende o caracteriza estos canales de ruido y define sus inversas. En lugar de incorporar las operaciones inversas en la ejecución del hardware (lo cual suele ser inviable), la PNA propaga los canales de ruido inversos a lo largo del circuito y los aplica a la observable. Este proceso transforma el operador observable $O$ en un nuevo operador $\\tilde{O}$, de tal forma que la medición de $\\tilde{O}$ en el circuito con ruido arroja el mismo valor esperado que la medición de $O$ en un circuito ideal, libre de ruido.\n",
        "\n",
        "Esto se puede resumir así:\n",
        "\n",
        "1. Modela cada puerta con ruido $u_i$ como una operación ideal seguida de un canal de ruido $\\Lambda_i$.\n",
        "2. Determina o calcula cada $\\Lambda_i$ utilizando herramientas de caracterización del ruido.\n",
        "3. Definir y propagar los mapas de ruido inversos $\\Lambda_i^{-1}$ a lo largo del circuito utilizando técnicas de transferencia de Pauli.\n",
        "4. Incorpora estas inversas en el observable, lo que da como resultado un operador con ruido atenuado $\\tilde{O}$.\n",
        "\n",
        "Cuando el ruido se describe como un canal de Pauli (o, de forma más general, como un canal de Pauli-Lindblad disperso), esta propagación puede llevarse a cabo de manera eficiente mediante la propagación de Pauli. La propagación de Pauli ofrece un marco para aproximar cómo se transforman los canales de ruido inversos a medida que atraviesan capas de operaciones de Clifford y no Clifford, al tiempo que se controla la complejidad computacional.\n",
        "\n",
        "Al trasladar la mitigación al ámbito observable, la PNA evita la elevada sobrecarga de muestreo que, de otro modo, se produciría al insertar operaciones de corrección física en el circuito. En su lugar, se ejecuta el circuito ruidoso original, mientras que el observable se transforma en un nuevo operador $\\tilde{O}$ cuyo valor esperado anula los efectos del ruido.\n",
        "\n",
        "<span id=\"the-pna-workflow\" />\n",
        "\n",
        "### El flujo de trabajo de la PNA\n",
        "\n",
        "El proceso puede entenderse a través de las siguientes etapas conceptuales. El primer esquema ilustra un experimento estándar con ruido.\n",
        "\n",
        "![Experimento ruidoso](https://quantum.cloud.ibm.com/docs/images/tutorials/propagated-noise-absorption/noisy_expt.avif)\n",
        "\n",
        "Si aprendemos el modelo de ruido, podemos aplicar su inversa y eliminar el ruido.\n",
        "\n",
        "![Experimento con reducción del ruido](https://quantum.cloud.ibm.com/docs/images/tutorials/propagated-noise-absorption/noise_mitigated_expt.avif)\n",
        "\n",
        "En lugar de implementar el canal de ruido inverso mediante muestreo en la QPU, como ocurre en la cancelación probabilística de errores (PEC), lo aplicamos de forma clásica al observable medido utilizando la propagación de Pauli. El observable resultante mitiga de forma efectiva el ruido de la puerta aprendido cuando se mide.\n",
        "\n",
        "![Descripción general de la 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",
        "### Mitigación modular de errores con Samplomatic y Executor\n",
        "\n",
        "El enfoque PNA se basa en una arquitectura modular de mitigación de errores de Qiskit. Esta arquitectura utiliza la [`samplomatic`](https://github.com/Qiskit/samplomatic) biblioteca junto con las `QuantumProgram` clases y `Executor` (añadidas a Qiskit Runtime en `qiskit-ibm-runtime`v0.47.0 ) para que técnicas como la absorción de ruido propagado y el giro de Pauli sean combinables y reutilizables en diferentes experimentos.\n",
        "\n",
        "En lugar de integrar la lógica de mitigación en la propia definición del circuito, la mitigación se expresa de forma declarativa mediante `samplomatic` anotaciones y se gestiona mediante programación a través de `Executor`, que controla cómo se generan, ejecutan y procesan posteriormente los circuitos aleatorios.\n",
        "\n",
        "En este tutorial implementamos un [patrón de Qiskit](/docs/guides/intro-to-patterns) para demostrar cómo PNA puede propagar canales de ruido de Pauli inversos y modificar el observable en consecuencia, con el fin de mejorar la estimación de los valores esperados en QPU con ruido.\n",
        "\n",
        "<span id=\"workflow-overview\" />\n",
        "\n",
        "### Resumen del flujo de trabajo\n",
        "\n",
        "* **Paso 1: Traslación al problema cuántico**\n",
        "  * Construye un modelo de Ising «kicked» trotterizado con simetría especular y una observable objetivo.\n",
        "* **Paso 2: Caracterizar y propagar el ruido**\n",
        "  * Utiliza `samplomatic` para identificar y anotar las capas únicas de dos qubits y las mediciones del circuito.\n",
        "  * Descubre el ruido que afecta a cada capa concreta utilizando `NoiseLearnerV3`.\n",
        "  * Asigna cada `InjectNoise` anotación a su modelo de ruido aprendido correspondiente.\n",
        "  * Utiliza `qiskit-addon-pna` para propagar los canales de ruido inversos hacia adelante a través del circuito e integrarlos en el observable objetivo.\n",
        "* **Paso 3: Realizar experimentos cuánticos**\n",
        "  * Define un `QuantumProgram` para especificar un muestreo aleatorio mediante `samplex` y ejecuta los experimentos en el backend utilizando el `Executor`.\n",
        "* **Paso 4: Reconstruir y analizar los resultados**\n",
        "  * Compara las estrategias de mitigación (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) y visualiza la mejora con respecto a los resultados sin medidas de mitigación.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3b64ad83-0000-4000-8000-000000000003",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de empezar este tutorial, asegúrate de tener instalado lo siguiente:\n",
        "\n",
        "* Qiskit SDK v2.2 o posterior, con soporte [para visualización](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.47 o posterior (`pip install qiskit-ibm-runtime`)\n",
        "* Samplomatic v0.13 o o posterior (`pip install samplomatic`)\n",
        "* Complemento PNA Qiskit (`pip install qiskit-addon-pna`)\n",
        "* Utilidades del complemento de Qiskit (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "28ad8a65-0000-4000-8000-000000000004",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuración\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",
        "## Ejemplo de simulador a pequeña escala\n",
        "\n",
        "La PNA mitiga el ruido *físico* de las puertas de dos qubits de un procesador cuántico concreto. El flujo de trabajo depende de dos servicios de hardware que no tienen un equivalente significativo en un simulador ideal:\n",
        "\n",
        "* `NoiseLearnerV3` caracteriza experimentalmente el canal de ruido de Pauli-Lindblad disperso asociado a cada capa única de dos qubits del circuito transpilado. En un simulador sin ruido no hay ruido que aprender, y el canal inverso propagado sería el canal de identidad.\n",
        "* La función `Executor` «primitive» muestrea los circuitos aleatorios y retorcidos generados por `samplomatic` en un backend.\n",
        "\n",
        "En principio, se podría sustituir por un modelo de ruido sintético. Por ejemplo, podrías adjuntar `PauliLindbladError` instrucciones con [Qiskit Aer](https://github.com/Qiskit/qiskit-aer) y pasar el circuito con ruido resultante directamente a `generate_noise_mitigating_observable`. Sin embargo, esto solo valida la contabilidad clásica frente al ruido que tú mismo has introducido, y desvirtúa el objetivo de la técnica. Por ese motivo, nos saltamos el ejemplo del simulador y mostramos el flujo de trabajo completo de PNA directamente en el hardware, desglosando a continuación cada paso del patrón de Qiskit.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "306f6858-0000-4000-8000-000000000007",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Ejemplo de hardware a gran escala\n",
        "\n",
        "Ahora ejecutamos el flujo de trabajo completo de PNA en un modelo de Ising de 30 sitios con «kicking», ejecutado en un hardware de l IBM Quantum®, siguiendo los cuatro pasos de un patrón de Qiskit.\n",
        "\n",
        "<span id=\"step-1-map-to-a-quantum-problem\" />\n",
        "\n",
        "### Paso 1: Traslación a un problema cuántico\n",
        "\n",
        "**Genera el circuito de Trotter reflejado y el observable**\n",
        "\n",
        "En este experimento, estudiaremos la dinámica temporal de un modelo de Ising con 30 sitios y «kicked» en una cadena de espines « 1D ». El hamiltoniano que se considera es:\n",
        "\n",
        "$H = -J\\sum\\limits_{\\langle i,j \\rangle} Z_iZ_j + h\\sum\\limits_iX_i$,\n",
        "\n",
        "donde $J>0$ describe el acoplamiento entre los espines más próximos, $i<j$, y el campo transversal global, $h$, se establece en $\\frac{\\pi}{8}$. Cuanto más se aleja $h$ de un ángulo de Clifford (es decir, $\\theta=n\\frac{\\pi}{2}, n \\in \\mathbb{Z}$ ), más difícil resulta propagar los generadores de antirruido a través del circuito.\n",
        "\n",
        "Para la elección de la magnitud observable, consideraremos la magnetización media por sitio, $\\frac{1}{N} \\sum_{i=1}^{N} \\langle z_i \\rangle$, donde $N$ es el número de sitios.\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",
        "### Paso 2: Optimizar el problema para su ejecución en hardware\n",
        "\n",
        "El siguiente paso consiste en preparar nuestro circuito Trotter duplicado para su ejecución en un hardware real de IBM Quantum. Para ejecutar un programa en una QPU no basta con construir el circuito abstracto, ya que debemos optimizarlo para que:\n",
        "\n",
        "1. **Respeta el conjunto de puertas y la conectividad nativas del backend.**\n",
        "   La transpilación mapea el circuito lógico a un circuito ISA compatible con el backend de destino. Esto garantiza que cada interacción entre una puerta y un qubit sea físicamente realizable.\n",
        "\n",
        "2. **Podemos caracterizar el ruido a nivel de las capas del circuito.**\n",
        "   La PNA se basa en el aprendizaje y la propagación de canales de ruido inversos. Para hacerlo de forma eficiente, dividimos el circuito transpilado en capas «encajonadas» únicas de dos qubits. Estas casillas nos permiten asociar cada capa del circuito con su propio modelo de ruido aprendido.\n",
        "\n",
        "3. **Podemos introducir modelos de ruido realistas en PNA.**\n",
        "   Una vez que el circuito se ha encapsulado, utilizamos el `NoiseLearnerV3` servicio para determinar experimentalmente los canales de ruido de Pauli que afectan a cada capa única de dos qubits. A continuación, estos modelos entrenados se vinculan de nuevo al circuito a través de las anotaciones de Samplomatic.\n",
        "\n",
        "En definitiva, este paso salva la distancia entre un circuito especular idealizado y un circuito listo para su implementación en hardware con modelos de ruido aprendidos. Con esta configuración, el PNA puede propagar canales de ruido inversos a través del circuito y ajustar la observable en consecuencia.\n",
        "\n",
        "<span id=\"connect-to-the-backend-and-transpile-to-an-isa-circuit\" />\n",
        "\n",
        "#### Conectarse al backend y compilarlo en un circuito ISA\n",
        "\n",
        "En primer lugar, inicializamos el servicio Qiskit Runtime y seleccionamos un backend. Inicia sesión con tu propia cuenta siguiendo las [instrucciones para guardar tus credenciales](/docs/guides/cloud-setup); a partir de ese `QiskitRuntimeService()` momento, el sistema las recuperará automáticamente.\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 the Qiskit Runtime 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": [
        "A continuación, elegimos una cadena conectada de qubits en el backend y compilamos el circuito en ella.\n",
        "\n",
        "Utilizando la `find_qubit_chain` función auxiliar definida en la sección «Configuración», seleccionamos una línea de `num_qubits` qubits físicos conectados. A continuación, realizamos la transpilación con `optimization_level=0` y fijamos esta cadena como el `initial_layout`, lo que conserva exactamente la estructura por capas de puertas de dos qubits del circuito reflejado. Esa estructura es en la que se basan las etapas de «boxing» y de aprendizaje de ruido, por lo que aquí hay que evitar un nivel de optimización más alto (que anularía las puertas reflejadas).\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",
        "### Gira las capas de puertas de dos qubits y las mediciones, y encuentra las capas únicas\n",
        "\n",
        "Utilizamos `samplomatic` para delimitar el circuito e identificar capas únicas de dos qubits. Un bloque es una estructura que agrupa instrucciones para que, posteriormente, se puedan aplicar de manera uniforme determinadas intenciones o anotaciones a todas las puertas que lo componen.\n",
        "\n",
        "`generate_boxing_pass_manager`En este caso, llamamos al método, que va más allá de la simple identificación de capas de dos qubits. Desempeña varias funciones clave:\n",
        "\n",
        "* Agrupa todas las capas de dos qubits del circuito,\n",
        "* Aplica las `Twirl` anotaciones y `ChangeBasis` a esas capas,\n",
        "* Agrupa las operaciones de medición en sus propias secciones enmarcadas, y\n",
        "* Aplica la `InjectNoise` anotación a cada capa de dos qubits.\n",
        "\n",
        "Estas anotaciones definen cómo se gestionan el ruido, los cambios de base y el giro a lo largo de todo el circuito. Además, establecen la estructura que se utilizará posteriormente para el aprendizaje y la reducción del ruido.\n",
        "\n",
        "Las opciones de configuración más importantes son:\n",
        "\n",
        "* `enable_gates`/`enable_measures: True`: Enmarca todas las capas de puertas de dos qubits y las mediciones finales. Las puertas de un solo qubit se representan con la orientación «left-dressed» dentro de los recuadros.\n",
        "* `measure_annotations: all`: Incluye `Twirl` las anotaciones y `ChangeBasis` en el recuadro de medidas.\n",
        "* `twirling_strategy: active`: Gira todos los qubits activos de cada casilla que contenga puertas de entrelazamiento.\n",
        "* `Twirl``inject_noise_targets: gates`: Añadir `InjectNoise` anotaciones a todos los recuadros anotados que contengan puertas de entrelazamiento.\n",
        "* `inject_noise_strategy: uniform_modification`: Ajusta el volumen de todas las capas de ruido de forma equivalente en todo el 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": [
        "Genera el circuito de plantilla y `samplex`, que definen cómo se muestreará el circuito.\n",
        "\n",
        "Aquí también añadimos las mediciones de espectador y de postselección, que son necesarias para llevar a cabo la postselección en las muestras generadas por el `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",
        "### Aprende el sonido utilizando `NoiseLearnerV3`\n",
        "\n",
        "Antes de poder aplicar el PNA para la mitigación de errores, primero debemos caracterizar el ruido que actúa sobre cada una de las capas de dos qubits y sobre la capa de medición de nuestro circuito. Para ello, utilizamos el `NoiseLearnerV3` programa para aprender de forma experimental los modelos de ruido de cada capa identificada anteriormente. El alumno lleva a cabo experimentos de tipo «benchmarking» que estiman el ruido del canal que afecta a cada capa y devuelve un objeto de resultado que contiene el modelo aprendido.\n",
        "\n",
        "Comenzamos identificando las capas únicas de nuestro circuito utilizando `find_unique_box_instructions` de `samplomatic`. Esto garantiza que solo aprendamos el ruido una vez por cada tipo de capa distinto, lo que minimiza el número de experimentos y el coste total de las ejecuciones. La lista de capas resultante se envía al modelo de aprendizaje de ruido.\n",
        "\n",
        "Hay algunos parámetros clave que determinan cómo se aprende el ruido:\n",
        "\n",
        "* `num_randomizations`: Número de circuitos aleatorios utilizados por configuración de aprendizaje.\n",
        "* `shots_per_randomization`: Número de disparos realizados por cada circuito de aprendizaje aleatorio.\n",
        "* `layer_pair_depths`: Las profundidades del circuito (medidas en número de pares) que se utilizarán en los experimentos de aprendizaje.\n",
        "* `post_selection`: Permite la postselección basada en flancos mediante `rx` puertas para aplicar pulsos posteriores a la medición.\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",
        "### Visualizar las tasas de ruido aprendidas\n",
        "\n",
        "Una vez aprendidos los modelos de ruido, podemos examinar la distribución de las tasas de error inferidas tanto para las operaciones de un qubit como para las de dos qubits. El código que figura a continuación extrae las representaciones de Pauli-Lindblad a partir de los resultados del ruido aprendido y recopila las tasas de ruido correspondientes.\n",
        "\n",
        "Para cada capa entrenada:\n",
        "\n",
        "* Convertimos el modelo de ruido en una lista dispersa de `(pstr, qubits, rate)` tuplas, donde `pstr` es la cadena de Pauli que actúa sobre los qubits dados y `rate` es la tasa de error asociada.\n",
        "* Desglosamos las tasas en términos de un qubit (`len(pstr) == 1`) y de dos qubits (`len(pstr) == 2`).\n",
        "* A continuación, se ordenan las listas de tasas y se calculan sus valores medianos.\n",
        "\n",
        "Representamos gráficamente las distribuciones de la tasa de ruido de un qubit (rojo) y de dos qubits (azul) en una escala logarítmica, con sus valores medianos marcados por líneas verticales, de modo que podamos comparar las magnitudes relativas de los generadores de Pauli-Lindblad aprendidos. El orden de las tasas de un qubit y de dos qubits depende del dispositivo y de las capas específicas que se estén caracterizando; en esta serie de pruebas, los generadores de un solo qubit ( weight-1 ) presentan la tasa mediana más elevada.\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",
        "### Relacionar las cajas de circuitos con el ruido aprendido\n",
        "\n",
        "Una vez que hayamos obtenido los modelos de ruido para cada capa única de dos qubits, debemos vincularlos a las anotaciones correspondientes `InjectNoise` dentro del circuito enmarcado.\n",
        "\n",
        "La `InjectNoise` directiva es una `samplomatic` anotación que utiliza los «dressers» de un solo qubit para inyectar ruido en el circuito de forma controlada y configurable. Permite realizar una modelización modular del ruido en diferentes capas.\n",
        "\n",
        "Cada `InjectNoise` anotación incluye:\n",
        "\n",
        "* `InjectNoise.ref` - un identificador único para la anotación. El `samplex` objeto utiliza esto para asignar correctamente el modelo de ruido correspondiente.\n",
        "* `InjectNoise.modifier_ref` *(opcional)* - una referencia secundaria que permite escalar el modelo de ruido asignado mediante un factor multiplicativo.\n",
        "\n",
        "En este paso, establecemos una correspondencia entre cada `InjectNoise.ref` y su modelo de ruido aprendido correspondiente (`PauliLindbladMap`). Esta correspondencia garantiza que cada capa de puertas de entrelazamiento del circuito se asocie al modelo de ruido adecuado, de modo que los efectos del ruido se apliquen con precisión durante el muestreo y los pasos posteriores de mitigación del ruido.\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",
        "### Propagar la variable observable a través del antirruido aprendido\n",
        "\n",
        "Como se ha comentado anteriormente, esto se lleva a cabo en dos pasos. En primer lugar, extendemos un generador antirruido hasta el final del circuito. A continuación, propagamos el observable a través de ese generador evolucionado. Este proceso se repite para cada generador antirruido del circuito. En esta implementación, cada generador de una capa determinada se propaga en paralelo hasta el final del circuito. Además, se utiliza el multiprocesamiento « Python » para realizar en paralelo tanto la propagación hacia adelante del antirruido como la propagación hacia atrás de la observable. Esto evita que se acumulen generadores evolucionados en la memoria y, además, optimiza al máximo los recursos de cálculo.\n",
        "\n",
        "Al ejecutar PNA, siempre hay que proporcionar un circuito con ruido y una variable observable. Si tu circuito ruidoso es un circuito en recuadro con `InjectNoise` anotaciones, debes proporcionar la correspondencia que hemos creado en el paso anterior. También se puede pasar un circuito no empaquetado que contenga `PauliLindbladError` instrucciones procedentes de `qiskit-aer`. En ese caso, `refs_to_noise_models` no es necesario indicarlo. Además de los datos principales, ten en cuenta también lo siguiente:\n",
        "\n",
        "* `max_err_terms`: El número de términos que se deben conservar en cada generador antirruido a medida que se propaga hacia adelante. Aumentar este valor suele mejorar la precisión, pero no se garantiza que este comportamiento sea monótono.\n",
        "* `max_obs_terms`: El número de términos que se deben mantener en la observable de mitigación del ruido, $\\tilde{O}$, a medida que se propaga hacia atrás a través del antirruido evolucionado. Por lo general, los valores más altos aumentan la precisión, pero no se garantiza que lo hagan de forma monótona.\n",
        "* `num_processes`: El número de núcleos que se van a dedicar al proceso. Recuerda que los generadores se propagan hacia adelante y se aplican a la observable en paralelo.\n",
        "* `search_step`: El paso de retropropagación utiliza un método «greedy» para conjugar de forma aproximada dos operadores en la base de Pauli. Este método puede acelerarse aumentando `search_step`. Ver el [`pauli-prop` Consulte la documentación](https://qiskit.github.io/pauli-prop/) para obtener más información.\n",
        "* `num_to_measure`: Aunque esta variable no es un parámetro de entrada de `generate_noise_mitigating_observable`, la utilizamos para controlar cuántos términos de $\\tilde{O}$ queremos medir realmente. Aquí solo medimos los 30 términos principales, que son los términos originales de nuestro observable. Los términos se han reescalado de tal forma que su medición tiene como efecto mitigar el ruido de puerta aprendido. Aunque solo medimos 30 términos de $\\tilde{O}$, a menudo sigue siendo útil dejar que el número de términos aumente, ya que eso aumenta la precisión de los factores de escala de los términos principales.\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",
        "### Transforma las bases de medida a forma canónica\n",
        "\n",
        "A continuación, buscamos un conjunto mínimo de bases de medición que nos permita cubrir por completo todos los términos de Pauli en la observable medida ( *se pueden medir muchas observables simultáneamente si conmutan a nivel de qubit* ). Dado que solo estamos midiendo los términos de nuestro observable original —que es la suma de todos los Paulis individuales—`Z` , solo se necesita una base: la base «todo`Z` ».\n",
        "\n",
        "Además de hallar un conjunto de bases de medición de Pauli, debemos expresar estos términos de Pauli en la forma canónica que espera el `Executor`. Para obtener más información sobre el orden canónico de los qubits, consulta la [documentación de 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",
        "### Paso 3: Realizar experimentos cuánticos\n",
        "\n",
        "<span id=\"specify-how-to-sample-in-the-quantumprogram\" />\n",
        "\n",
        "#### Especifica cómo realizar el muestreo en el `QuantumProgram`\n",
        "\n",
        "Ahora configuramos el `QuantumProgram`, que actúa como contenedor central de todos los circuitos y configuraciones de muestreo que ejecutará el `Executor`. Este objeto define cómo se generan, se agrupan y se ejecutan las instancias de circuitos aleatorias para obtener los resultados de medición utilizados en PNA.\n",
        "\n",
        "Un `QuantumProgram` puede contener varios elementos, cada uno de los cuales consta de un circuito de plantilla y un objeto correspondiente `samplex` que define cómo se aplican las aleatorizaciones. Esta abstracción permite que el `Executor` programa gestione todo el flujo de trabajo como un único programa modular, desde la generación aleatoria de circuitos hasta la recopilación y agregación de tomas.\n",
        "\n",
        "En este paso, creamos un `QuantumProgram` que ejecuta nuestro experimento PNA utilizando el circuito modelo y `samplex` que hemos construido anteriormente. La configuración incluye los siguientes elementos:\n",
        "\n",
        "* `template_circuit`: El circuito que contiene todas las puertas lógicas necesarias para implementar todas las aleatorizaciones deseadas (desde aleatorizaciones de tipo «twirling» hasta parámetros, etc.).\n",
        "* `samplex`: Un objeto que define una distribución de probabilidad sobre todas las posibles aleatorizaciones del circuito, de las que se tomarán muestras.\n",
        "* `samplex_arguments`: Las relaciones necesarias para definir completamente el `samplex`\n",
        "  * `basis_changes`: Aquí es donde especificamos un conjunto de bases de medición que cubra todos los términos de Pauli en el observable medido.\n",
        "  * `noise_scales.ref`: Ajustamos la escala de cada capa de ruido a `0.0` para evitar que se introduzca ruido adicional en nuestras muestras.\n",
        "  * `pauli_lindblad_maps`: Obligatorio si `noise_scales` se pasan. Esto simplemente asigna las capas de ruido al modelo de ruido correspondiente.\n",
        "* `shape`: Una tupla de forma para ampliar la forma implícita definida por `samplex_arguments`. Los ejes no triviales introducidos por esta extensión enumeran las aleatorizaciones.\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",
        "#### Mide la tensión del circuito utilizando el `Executor`\n",
        "\n",
        "Ahora que hemos definido nuestro `QuantumProgram`, llevar a cabo el experimento es muy sencillo. Simplemente creamos una instancia del `Executor` objeto, le asignamos el backend y ejecutamos el programa.\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",
        "### Paso 4: Reconstruir y analizar los resultados\n",
        "\n",
        "Para calcular un valor esperado con error mitigado, procedemos de la siguiente manera:\n",
        "\n",
        "* Calcular los factores de escalado de TREX basándose en el ruido detectado que afecta a las mediciones,\n",
        "* Genera una máscara para conservar únicamente las muestras seleccionadas posteriormente, y\n",
        "* Utiliza la `executor_expectation_values` función de `qiskit-addon-utils` para combinar todos los datos en un valor esperado con el error minimizado.\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",
        "#### Comparar estrategias de mitigación: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX\n",
        "\n",
        "A partir de los `Executor` resultados, calculamos y visualizamos los valores esperados para varias variantes de mitigación.\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": [
        "Los resultados ponen de manifiesto las ventajas acumulativas que supone combinar diferentes técnicas de mitigación de errores. El enfoque PNA básico ya restaura el valor esperado a un nivel cercano al de referencia ideal, lo que indica que la propagación de canales de ruido inversos en la observable compensa eficazmente los errores de las puertas de dos qubits.\n",
        "\n",
        "* La incorporación de la reponderación TREX (PNA+TREX) mejora ligeramente la estimación al corregir el desequilibrio de muestreo en los circuitos aleatorizados.\n",
        "* La postselección (PNA+PS) proporciona una mejora más notable al filtrar los resultados de medición incoherentes que probablemente se deban a errores residuales.\n",
        "* Por último, la combinación de ambas (PNA+PS+TREX) ofrece el resultado más preciso, que se aproxima mucho al valor ideal, lo que demuestra cómo estas estrategias de mitigación se refuerzan mutuamente.\n",
        "\n",
        "En general, la comparación pone de manifiesto que PNA constituye una base sólida para la estimación del valor esperado teniendo en cuenta el ruido, mientras que TREX y la postselección ofrecen mejoras complementarias para obtener una mayor precisión.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1b2c3d4-9999-4000-8000-000000000099",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Próximos pasos\n",
        "\n",
        "Si este trabajo te ha parecido interesante, quizá te interese el siguiente material:\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendaciones\">\n",
        "  * [Combinar opciones de mitigación de errores con la primitiva Estimator](/docs/tutorials/combine-error-mitigation-techniques)\n",
        "  * [Mitigación de errores a gran escala con amplificación probabilística de errores](/docs/tutorials/probabilistic-error-amplification)\n",
        "  * [Retropropagación del operador (OBP) para la estimación de valores esperados](/docs/tutorials/operator-back-propagation)\n",
        "  * La documentación de [`samplomatic`](https://github.com/Qiskit/samplomatic) y [`pauli-prop`](https://github.com/Qiskit/pauli-prop)\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
}