{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "frontmatter",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Detección de errores con baja sobrecarga mediante códigos espaciotemporales\"\n",
        "description: \"Inserta automáticamente comprobaciones de Pauli en el espacio-tiempo con qiskit-paulice y mejora la fidelidad de los circuitos de Clifford muestreados mediante la postselección de síndromes de comprobación.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore postsel, frameon, virt */}\n",
        "\n",
        "<span id=\"low-overhead-error-detection-with-spacetime-codes\" />\n",
        "\n",
        "# Detección de errores con baja sobrecarga mediante códigos espaciotemporales\n",
        "\n",
        "*Tiempo de ejecución estimado: 4 minutos en un procesador Heron (`ibm_kingston` o equivalente) (NOTA: Se trata únicamente de una estimación). (El tiempo de ejecución puede variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "learning-outcomes",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados del aprendizaje\n",
        "\n",
        "* Cómo las comprobaciones de Pauli en el espaciotiempo detectan errores lógicos en los circuitos de Clifford, y cómo la postselección basada en sus síndromes aumenta la fidelidad de una distribución muestreada.\n",
        "* Cómo utilizar el paquete `qiskit-paulice` para buscar e insertar comprobaciones eficientes a nivel de hardware de forma automática con `get_check_qubits`, `NoiseModel`, y `add_pauli_checks`.\n",
        "* Cómo estimar la fidelidad de un estado estabilizador mediante el muestreo de sus estabilizadores y la postselección basada en síndromes de comprobación.\n",
        "* Cómo ejecutar el flujo de trabajo completo de detección de errores en un hardware de tip IBM Quantum® e y comparar las fidelidades con ruido y las postseleccionadas.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "prerequisites",
      "metadata": {},
      "source": [
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Requisitos previos\n",
        "\n",
        "* [Fundamentos de hardware](/learning/courses/utility-scale-quantum-computing/hardware) para la computación cuántica a escala industrial.\n",
        "* El formalismo de Clifford y estabilizadores, incluyendo cómo un grupo estabilizador describe un estado estabilizador puro.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "background",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## En segundo plano\n",
        "\n",
        "[El artículo «Detección de errores con baja sobrecarga mediante códigos espaciotemporales»](https://arxiv.org/abs/2504.15725) [\\[1\\],](#references) de Simon Martiel y Ali Javadi-Abhari, presenta un método para detectar errores lógicos en circuitos dominados por Clifford que se sitúa a medio camino entre la corrección completa de errores y la mitigación de errores, que supone una menor carga. La idea se basa en las comprobaciones coherentes de Pauli (CPC) descritas en el artículo [«Mitigación de errores en un solo disparo mediante comprobaciones coherentes de Pauli»](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.5.033193) [\\[2\\],](#references) de van den Berg y otros. En ambos enfoques, un circuito «de carga útil» de Clifford se entrelaza con qubits auxiliares para comprobar determinados invariantes. La medición de las ancillas genera un síndrome que indica si se ha detectado un error durante la ejecución. Conservar únicamente las muestras en las que no se ha detectado ningún error mejora la fidelidad de la distribución muestreada, a costa de una menor tasa de postselección.\n",
        "\n",
        "La diferencia fundamental entre los controles coherentes de Pauli y los controles espaciotemporales radica en los operadores que miden. Las comprobaciones de Pauli coherentes miden operadores localizados en el tiempo y de gran peso. En las topologías de qubits con conectividad limitada, como la «heavy hex», esas comprobaciones requieren muchas puertas SWAP y, a menudo, hacen que el circuito sea demasiado profundo como para ejecutarlo en la práctica. En cambio, al implementar las comprobaciones como códigos espaciotemporales, cada comprobación se distribuye a lo largo del circuito de carga útil, tanto en el espacio como en el tiempo. Esto da lugar a una codificación eficiente desde el punto de vista del hardware que sigue siendo eficaz a la hora de detectar errores lógicos, al tiempo que mantiene baja la sobrecarga de qubits y de profundidad.\n",
        "\n",
        "<span id=\"what-the-qiskit-paulice-package-does\" />\n",
        "\n",
        "### Qué hace el paquete `qiskit-paulice`\n",
        "\n",
        "El paquete `qiskit-paulice` automatiza la creación de estas comprobaciones, de modo que no tengas que crearlas manualmente. Su función principal consiste en encontrar e insertar comprobaciones de Pauli válidas en el espacio-tiempo en aquellos puntos del circuito que maximicen la detección de errores y, al mismo tiempo, minimicen la sobrecarga de los qubits. Una comprobación es *válida* cuando sus operadores no modifican la acción lógica del circuito de carga útil, *se considera de bajo peso* cuando utiliza pocas puertas de entrelazamiento y *es eficaz* cuando detecta una gran parte de los errores, en relación con el ruido que la propia comprobación introduce. El paquete evalúa las comprobaciones candidatas en función de un modelo de ruido e incorpora las mejores al circuito. En este tutorial se utilizan tres métodos de la API:\n",
        "\n",
        "* `get_check_qubits` inspecciona un mapa de acoplamiento del backend y devuelve pares de qubits objetivo y auxiliares. Una comprobación de utiliza `target_qubits[i]` `ancilla_qubits[i]`.\n",
        "* `NoiseModel.from_backend` crea un modelo aproximado de ruido a partir de los datos de las pruebas de rendimiento del backend. El modelo evalúa las comprobaciones de los candidatos, por lo que no es necesario disponer de un modelo de ruido exacto y entrenado. Para obtener más información sobre un modelo de Pauli-Lindblad entrenado, véase [`NoiseModel.from_pauli_lindblad_maps`](/docs/api/qiskit-addon-paulice/qiskit-paulice-noise-models#from_pauli_lindblad_maps).\n",
        "* `add_pauli_checks` localiza e inserta controles en un circuito. Devuelve una secuencia de `CheckedCircuit` objetos con un número creciente de comprobaciones, y cada objeto proporciona una función `get_postselection_method` que asocia una cadena de bits medida a un vector de síndrome. El argumento `cost` selecciona la función que evalúa una comprobación (`gamma`, la sobrecarga de muestreo del canal de ruido inverso postseleccionado, o `LER`, la tasa de error lógico). El argumento `method` selecciona la estrategia de búsqueda (`windowed`, `genetic`, o `windowed_genetic`). Este tutorial utiliza y `cost=\"gamma\"` `method=\"windowed\"`, que, combinados, permiten una selección de comprobaciones determinista y reproducible.\n",
        "\n",
        "<span id=\"estimate-fidelity-from-stabilizer-sampling\" />\n",
        "\n",
        "### Estimación de la fidelidad a partir del muestreo del estabilizador\n",
        "\n",
        "Para evaluar el rendimiento del sistema de detección de errores, se puede estimar la fidelidad entre el estado del estabilizador $|\\psi\\rangle = U|0\\rangle^{\\otimes n}$ —que el circuito prepara idealmente— y el estado con ruido $\\rho$ —que el hardware genera realmente—. La proyección sobre un estado puro del estabilizador $|\\psi\\rangle$ es igual a la media uniforme sobre los elementos $2^n$ de su grupo estabilizador $\\mathcal{S}$ :\n",
        "\n",
        "$|\\psi\\rangle\\langle\\psi| = \\frac{1}{2^n}\\sum_{G \\in \\mathcal{S}} G.$\n",
        "\n",
        "Al sustituir esto en la fórmula de la fidelidad, se obtiene que la fidelidad de $\\rho$ es el valor esperado medio de cada estabilizador $G \\in \\mathcal{S}$ con respecto a $\\rho$ :\n",
        "\n",
        "$F = \\mathrm{Tr}(\\rho|\\psi\\rangle\\langle\\psi|) = \\frac{1}{2^n} \\sum_{G \\in \\mathcal{S}} \\mathrm{Tr}(\\rho G) = \\frac{1}{2^n} \\sum_{G \\in \\mathcal{S}} \\langle G \\rangle_\\rho.$\n",
        "\n",
        "En el caso de problemas más complejos, no es viable enumerar todos los estabilizadores $2^n$, por lo que se puede estimar la fidelidad a partir de una muestra aleatoria. Al extraer de forma aleatoria y uniforme los estabilizadores $M$ $G_1, \\ldots, G_M$ de $\\mathcal{S}$ se obtiene una estimación insesgada:\n",
        "\n",
        "$\\hat F_M = \\frac{1}{M} \\sum_{i=1}^M \\langle G_i \\rangle_\\rho.$\n",
        "\n",
        "Dado que un circuito de Clifford prepara un estado estabilizador, se puede estimar su fidelidad directamente a partir de los valores esperados muestreados de sus estabilizadores. En este tutorial se explica primero el flujo de trabajo en un simulador con un circuito pequeño y, a continuación, se ejecuta el mismo flujo de trabajo en hardware con un circuito más grande y complejo. A medida que los circuitos incluyen más operaciones no Clifford, el número de comprobaciones válidas se reduce rápidamente, por lo que el método funciona mejor con circuitos dominados por operaciones Clifford.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "requirements",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de empezar este tutorial, asegúrate de tener instalados los siguientes elementos:\n",
        "\n",
        "* Qiskit SDK v2.0 o posterior, con soporte [para visualización](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.40 o posterior (`pip install qiskit-ibm-runtime`)\n",
        "* Qiskit Aer v0.17 o posterior (`pip install qiskit-aer`)\n",
        "* Qiskit Paulice (`pip install qiskit-paulice`)\n",
        "* tqdm (`pip install tqdm`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "setup",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuración\n",
        "\n",
        "Importa las bibliotecas necesarias y define las funciones auxiliares que no estén disponibles como importaciones. La función `random_clifford_circuit` genera una carga útil Clifford aleatoria de tipo «brickwork», `find_check_layout` busca en un mapa de acoplamiento del backend una ruta de qubits con bajo error y muchas ancillas disponibles, `learned_noise_model` convierte `NoiseLearner` la salida en un modelo de ruido `qiskit-paulice` , `append_basis_rotation` gira un circuito de modo que se mida un estabilizador en la base computacional, `expectation` calcula el valor esperado del estabilizador a partir de los recuentos muestreados y `cum_mean_sem` realiza un seguimiento de la estimación de fidelidad en tiempo real.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "setup-imports",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Standard library imports\n",
        "import random\n",
        "import time\n",
        "\n",
        "# External libraries\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "from tqdm import tqdm\n",
        "\n",
        "# Qiskit\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.quantum_info import Clifford, Pauli, PauliLindbladMap, PauliList\n",
        "from qiskit.result import sampled_expectation_value\n",
        "from qiskit.transpiler import generate_preset_pass_manager\n",
        "from qiskit.visualization import plot_coupling_map\n",
        "\n",
        "# Qiskit Aer\n",
        "from qiskit_aer import AerSimulator\n",
        "from qiskit_aer.noise import NoiseModel as AerNoiseModel\n",
        "from qiskit_aer.noise import ReadoutError, depolarizing_error\n",
        "\n",
        "# Qiskit IBM Runtime\n",
        "from qiskit_ibm_runtime import NoiseLearner, QiskitRuntimeService\n",
        "from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
        "\n",
        "# Qiskit Paulice\n",
        "from qiskit_paulice import add_pauli_checks\n",
        "from qiskit_paulice.layout import get_check_qubits\n",
        "from qiskit_paulice.noise_models import NoiseModel"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "setup-helpers",
      "metadata": {},
      "outputs": [],
      "source": [
        "def random_clifford_circuit(\n",
        "    num_qubits: int, depth: int, rng: np.random.Generator\n",
        ") -> QuantumCircuit:\n",
        "    \"\"\"Brickwork random Clifford on `num_qubits`, with `depth` CZ layers.\"\"\"\n",
        "    qc = QuantumCircuit(num_qubits)\n",
        "    qc.h(range(num_qubits))\n",
        "    for d in range(depth):\n",
        "        for i in range(d % 2, num_qubits - 1, 2):\n",
        "            qc.cz(i, i + 1)\n",
        "        for q in range(num_qubits):\n",
        "            if rng.integers(0, 2):\n",
        "                qc.sx(q)\n",
        "            if rng.integers(0, 2):\n",
        "                qc.s(q)\n",
        "            if rng.integers(0, 2):\n",
        "                qc.sx(q)\n",
        "    return qc\n",
        "\n",
        "\n",
        "def find_check_layout(\n",
        "    backend,\n",
        "    num_qubits: int,\n",
        "    rng: np.random.Generator,\n",
        "    num_trials: int = 200,\n",
        "    max_gate_error: float = 0.03,\n",
        "    max_readout_error: float = 0.2,\n",
        ") -> list[int]:\n",
        "    \"\"\"Find a low-error path of `num_qubits` qubits with many available ancillas.\n",
        "\n",
        "    Builds random self-avoiding walks on the coupling map, excluding the qubits\n",
        "    and two-qubit gates whose reported errors exceed the thresholds, and keeps\n",
        "    the path that offers the most target and ancilla pairs. Ties are broken by\n",
        "    the lower average two-qubit gate error along the path.\n",
        "    \"\"\"\n",
        "    target = backend.target\n",
        "    gate_2q = next(\n",
        "        name for name in (\"cz\", \"ecr\", \"cx\") if name in target.operation_names\n",
        "    )\n",
        "\n",
        "    # Collect per-edge gate errors and per-qubit readout errors\n",
        "    edge_error = {}\n",
        "    for qubits, props in target[gate_2q].items():\n",
        "        edge = tuple(sorted(qubits))\n",
        "        if props is not None and props.error is not None:\n",
        "            edge_error[edge] = min(edge_error.get(edge, 1.0), props.error)\n",
        "    readout_error = {\n",
        "        qubit: target[\"measure\"][(qubit,)].error\n",
        "        for (qubit,) in target[\"measure\"]\n",
        "    }\n",
        "\n",
        "    # Keep only the edges whose gate and readout errors are acceptable\n",
        "    adjacency = {}\n",
        "    for (q1, q2), error in edge_error.items():\n",
        "        if (\n",
        "            error <= max_gate_error\n",
        "            and readout_error.get(q1, 1.0) <= max_readout_error\n",
        "            and readout_error.get(q2, 1.0) <= max_readout_error\n",
        "        ):\n",
        "            adjacency.setdefault(q1, set()).add(q2)\n",
        "            adjacency.setdefault(q2, set()).add(q1)\n",
        "\n",
        "    # Random self-avoiding walks; keep the path with the most check pairs\n",
        "    starts = sorted(adjacency)\n",
        "    best_path = None\n",
        "    best_score = (-1, float(\"inf\"))\n",
        "    for _ in range(num_trials):\n",
        "        path = [starts[rng.integers(len(starts))]]\n",
        "        while len(path) < num_qubits:\n",
        "            options = sorted(adjacency[path[-1]] - set(path))\n",
        "            if not options:\n",
        "                break\n",
        "            path.append(options[rng.integers(len(options))])\n",
        "        if len(path) < num_qubits:\n",
        "            continue\n",
        "        num_pairs = len(get_check_qubits(backend.coupling_map, path)[0])\n",
        "        mean_error = float(\n",
        "            np.mean(\n",
        "                [edge_error[tuple(sorted(e))] for e in zip(path, path[1:])]\n",
        "            )\n",
        "        )\n",
        "        if num_pairs > best_score[0] or (\n",
        "            num_pairs == best_score[0] and mean_error < best_score[1]\n",
        "        ):\n",
        "            best_path, best_score = path, (num_pairs, mean_error)\n",
        "\n",
        "    if best_path is None:\n",
        "        raise RuntimeError(\n",
        "            \"No connected low-error path found. Relax the error thresholds.\"\n",
        "        )\n",
        "    return best_path\n",
        "\n",
        "\n",
        "def learned_noise_model(layer_errors, layout: list[int]) -> NoiseModel:\n",
        "    \"\"\"Build a `NoiseModel` from `NoiseLearner` results.\n",
        "\n",
        "    `NoiseLearner` reports one `PauliLindbladError` per entangling layer, whose\n",
        "    generators are indexed against that layer's own physical qubits, while\n",
        "    `NoiseModel.from_pauli_lindblad_maps` expects `PauliLindbladMap`s indexed the\n",
        "    way `NoiseModel.from_backend` indexes them: by position in `layout`. This\n",
        "    translates between the two and drops generators that fall outside `layout`.\n",
        "    \"\"\"\n",
        "    phys_to_virt = {phys: virt for virt, phys in enumerate(layout)}\n",
        "    maps = []\n",
        "    for layer in layer_errors:\n",
        "        if layer.error is None:\n",
        "            continue\n",
        "        terms = []\n",
        "        for pauli, rate in zip(\n",
        "            layer.error.generators, layer.error.rates, strict=True\n",
        "        ):\n",
        "            label, indices = [], []\n",
        "            for local, phys in enumerate(layer.qubits):\n",
        "                x, z = bool(pauli.x[local]), bool(pauli.z[local])\n",
        "                if not (x or z):\n",
        "                    continue\n",
        "                if phys not in phys_to_virt:\n",
        "                    break  # generator reaches outside the layout, so skip it\n",
        "                label.append(\"Y\" if x and z else \"X\" if x else \"Z\")\n",
        "                indices.append(phys_to_virt[phys])\n",
        "            else:\n",
        "                if label:\n",
        "                    terms.append(\n",
        "                        (\"\".join(label), tuple(indices), float(rate))\n",
        "                    )\n",
        "        # Each map needs a 2-qubit generator to define an entangling layer\n",
        "        if any(len(t[1]) == 2 for t in terms):\n",
        "            maps.append(\n",
        "                PauliLindbladMap.from_sparse_list(\n",
        "                    terms, num_qubits=len(layout)\n",
        "                )\n",
        "            )\n",
        "    if not maps:\n",
        "        raise RuntimeError(\n",
        "            \"No usable layer errors. Check that the learner ran on this layout.\"\n",
        "        )\n",
        "    return NoiseModel.from_pauli_lindblad_maps(maps)\n",
        "\n",
        "\n",
        "def append_basis_rotation(\n",
        "    circuit: QuantumCircuit, pauli: Pauli\n",
        ") -> QuantumCircuit:\n",
        "    \"\"\"Strip measurements, append basis rotations for `pauli`, and re-measure.\"\"\"\n",
        "    out = circuit.remove_final_measurements(inplace=False)\n",
        "    for q in range(pauli.num_qubits):\n",
        "        if pauli.x[q]:\n",
        "            if pauli.z[q]:\n",
        "                out.sdg(q)\n",
        "            out.h(q)\n",
        "    out.measure_all()\n",
        "    return out\n",
        "\n",
        "\n",
        "def expectation(counts: dict, pauli: Pauli) -> float:\n",
        "    \"\"\"Expectation value of `pauli` from counts measured in the Z basis.\n",
        "\n",
        "    Pads with identity on any qubits beyond the support of `pauli`, such as the\n",
        "    check ancillas that appear in the postselected counts.\n",
        "    \"\"\"\n",
        "    if not counts:\n",
        "        return float(\"nan\")\n",
        "    n = pauli.num_qubits\n",
        "    sign = -1 if int(pauli.phase) % 4 == 2 else 1\n",
        "    total = len(next(iter(counts)))\n",
        "    label = \"\".join(\n",
        "        \"Z\" if q < n and (pauli.x[q] or pauli.z[q]) else \"I\"\n",
        "        for q in range(total - 1, -1, -1)\n",
        "    )\n",
        "    return sign * sampled_expectation_value(counts, label)\n",
        "\n",
        "\n",
        "def cum_mean_sem(values: np.ndarray):\n",
        "    \"\"\"Cumulative mean and standard error of the mean, ignoring NaNs.\"\"\"\n",
        "    valid = ~np.isnan(values)\n",
        "    total = np.cumsum(np.where(valid, values, 0.0))\n",
        "    total_sq = np.cumsum(np.where(valid, values**2, 0.0))\n",
        "    count = np.maximum(np.cumsum(valid).astype(float), 1)\n",
        "    mean = total / count\n",
        "    sem = np.sqrt(np.maximum(total_sq / count - mean**2, 0) / count)\n",
        "    return np.where(np.cumsum(valid) > 0, mean, np.nan), sem"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "small-scale-header",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Ejemplo de simulador a pequeña escala\n",
        "\n",
        "En esta sección se explica paso a paso el flujo de trabajo completo en un simulador con ruido. Utiliza datos de pruebas de rendimiento del backend para elegir una disposición de qubits y un modelo de ruido, detecta automáticamente las comprobaciones y aplica la poselección a la distribución muestreada para mostrar la mejora de la fidelidad.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step1-header",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Paso 1: Asignar entradas clásicas a un problema cuántico\n",
        "\n",
        "El circuito de carga útil es un circuito de Clifford aleatorio unidimensional de estructura en ladrillo de poca profundidad. Dado que el circuito es de tipo Clifford, genera un estado estabilizador cuya fidelidad se puede estimar directamente a partir de los valores esperados muestreados del estabilizador. Empieza con un circuito poco profundo para que los puntos de comprobación sean fáciles de visualizar en el siguiente paso.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "step1-code",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step1-code-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "num_qubits = 12\n",
        "depth = 4\n",
        "seed = 1764\n",
        "rng = np.random.default_rng(seed)\n",
        "np.random.seed(seed)\n",
        "\n",
        "circuit = random_clifford_circuit(num_qubits, depth, rng)\n",
        "circuit.measure_all()\n",
        "circuit.draw(\"mpl\", fold=-1, scale=0.6)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step2-header",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-for-quantum-hardware-execution\" />\n",
        "\n",
        "### Paso 2: Optimizar para la ejecución en hardware cuántico\n",
        "\n",
        "Al asignar el circuito al hardware se definen la disposición física de los qubits, el modelo de ruido que evalúa las comprobaciones candidatas y las propias comprobaciones.\n",
        "\n",
        "En primer lugar, selecciona un backend y busca en su mapa de acoplamiento una disposición de qubits unidimensional con el `find_check_layout` «helper» definido en la sección «Configuración». El asistente genera recorridos aleatorios que evitan las puertas y las lecturas con mayor error, y selecciona la ruta que ofrece el mayor número de pares de objetivos y ancillas. Dado que la búsqueda lee los datos de conectividad y de errores directamente del propio backend, el mismo código se ejecuta en cualquier QPU de IBM Quantum. A continuación, la función `get_check_qubits` devuelve los pares de objetivo y ancilla, donde la comprobación de utiliza `target_qubits[i]` `ancilla_qubits[i]`.\n",
        "\n",
        "En el gráfico de acoplamiento que se muestra a continuación, los qubits verdes son los qubits de carga útil y los qubits naranjas son los qubits auxiliares que realizan las comprobaciones. Los qubits con un ancilla adyacente se utilizan como qubits objetivo para las comprobaciones.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "step2-layout",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Backend: ibm_boston\n",
            "Target qubits:  [105, 107, 108, 123, 125, 141, 143]\n",
            "Ancilla qubits: [104, 97, 109, 122, 126, 140, 144]\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step2-layout-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(operational=True, simulator=False)\n",
        "\n",
        "print(f\"Backend: {backend.name}\")\n",
        "\n",
        "# Search for a low-error path, then pair each target qubit with a neighboring ancilla\n",
        "layout = find_check_layout(backend, num_qubits, rng)\n",
        "target_qubits, ancilla_qubits = get_check_qubits(backend, layout)\n",
        "num_checks = len(target_qubits)\n",
        "\n",
        "print(f\"Target qubits:  {target_qubits}\")\n",
        "print(f\"Ancilla qubits: {ancilla_qubits}\")\n",
        "plot_coupling_map(\n",
        "    num_qubits=backend.num_qubits,\n",
        "    qubit_coordinates=getattr(\n",
        "        backend.configuration(), \"qubit_coordinates\", None\n",
        "    ),\n",
        "    coupling_map=backend.configuration().coupling_map,\n",
        "    figsize=(12, 12),\n",
        "    qubit_color=[\n",
        "        \"#4CAF50\"\n",
        "        if i in set(layout)\n",
        "        else \"#FF9800\"\n",
        "        if i in set(ancilla_qubits)\n",
        "        else \"#DDDDDD\"\n",
        "        for i in backend.coupling_map.graph.node_indices()\n",
        "    ],\n",
        "    qubit_size=220,\n",
        "    line_width=2,\n",
        "    font_size=90,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step2-transpile-md",
      "metadata": {},
      "source": [
        "Una vez elegidos el backend y el diseño, compila la carga útil en un circuito de arquitectura de conjunto de instrucciones (ISA). Solo hay que configurar el diseño y convertir las puertas al conjunto de puertas nativo del backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "step2-transpile",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step2-transpile-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 5,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "pm = generate_preset_pass_manager(\n",
        "    optimization_level=0, backend=backend, initial_layout=layout\n",
        ")\n",
        "circuit_isa = pm.run(circuit)\n",
        "circuit_isa.draw(\"mpl\", fold=-1, scale=0.6)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step2-noise-md",
      "metadata": {},
      "source": [
        "A continuación, muestra cómo el ruido de puerta y de lectura en el backend afecta a la ejecución. El modelo de ruido determina en qué punto del circuito una comprobación detecta la mayor cantidad de errores. Un modelo más preciso mejora la detección, pero normalmente no es necesario entrenarlo mediante muestreo de la QPU. El modelo que se presenta a continuación deduce, a partir de datos de referencia `qiskit-ibm-runtime` , un canal de despolarización uniforme para el ruido de la puerta y el ruido de lectura.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "step2-noise",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "NoiseModel(gate_noise=0.001079865281450939, readout_noise=0.006001790364583333, idling_noise=None)\n"
          ]
        }
      ],
      "source": [
        "noise_model = NoiseModel.from_backend(\n",
        "    backend, layout, uniform_gate_noise=True\n",
        ")\n",
        "print(noise_model)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step2-checks-md",
      "metadata": {},
      "source": [
        "Ahora añade los controles al circuito. La función `add_pauli_checks` toma como argumentos la carga útil de Clifford, la lista de qubits de destino y el modelo de ruido. El argumento `ancilla_qubits` indica a la función con qué ancilla física debe emparejarse cada objetivo. Las marcas se añaden en el orden en que aparecen los qubits de destino, por lo que la disposición final del circuito marcado es `layout + ancilla_qubits`. Para hacer funcionar un circuito de salida con menos (`i`) comprobaciones, la disposición final es `layout + ancilla_qubits[:i]`.\n",
        "\n",
        "El resultado de `add_pauli_checks` es una secuencia de circuitos con un número creciente de comprobaciones, desde ninguna comprobación hasta una comprobación por cada qubit objetivo. La visualización confirma que las comprobaciones utilizan los pares de objetivo y ancilla especificados. Para obtener más información sobre cómo encontrar buenos controles, véanse las secciones II a IV de la información complementaria de la referencia [\\[1\\]](#references).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "step2-checks",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Physical layout of payload and ancillas: [108, 107, 106, 105, 117, 125, 124, 123, 136, 143, 142, 141, 104, 97, 109, 122, 126, 140, 144]\n",
            "Checked circuit:\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step2-checks-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 7,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "checked = add_pauli_checks(\n",
        "    circuit_isa,\n",
        "    target_qubits,\n",
        "    noise_model,\n",
        "    ancilla_qubits=ancilla_qubits,\n",
        "    cost=\"gamma\",\n",
        "    method=\"windowed\",\n",
        "    seed=seed,\n",
        ")\n",
        "\n",
        "print(f\"Physical layout of payload and ancillas: {layout + ancilla_qubits}\")\n",
        "print(\"Checked circuit:\")\n",
        "checked[-1].circuit.draw(\"mpl\", fold=-1, idle_wires=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step3-header",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### Paso 3: Ejecutar con el comando « Qiskit primitives »\n",
        "\n",
        "Para hacer visible el efecto del ruido de la puerta, aumenta la profundidad de la carga útil y toma una muestra de un subconjunto de sus estabilizadores. Por lo general, cada estabilizador no conmuta con los demás a nivel de qubit, por lo que un único conjunto de comprobaciones no es válido para dos estabilizadores diferentes. En lugar de agrupar los estabilizadores en conjuntos de conmutación, busca un buen conjunto de comprobaciones para cada estabilizador por separado. La selección aleatoria y uniforme de estabilizadores proporciona una estimación insesgada de la fidelidad.\n",
        "\n",
        "Construye el circuito más complejo y selecciona una muestra aleatoria de sus estabilizadores.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "step3-stabilizers",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {ZXIIXZYYXIZZ, XXXYIIZYXIII, ...}\n"
          ]
        }
      ],
      "source": [
        "depth = 24\n",
        "num_stabilizers = 20\n",
        "num_shots = 1_000\n",
        "\n",
        "circuit = random_clifford_circuit(num_qubits, depth, rng)\n",
        "\n",
        "# Build the full stabilizer group, then sample from it uniformly at random\n",
        "circ_no_meas = circuit.remove_final_measurements(inplace=False)\n",
        "stabilizer_group = PauliList([Pauli(\"I\" * num_qubits)])\n",
        "for generator in (\n",
        "    Pauli(label) for label in Clifford(circ_no_meas).to_labels(mode=\"S\")\n",
        "):\n",
        "    stabilizer_group = stabilizer_group + stabilizer_group.compose(generator)\n",
        "\n",
        "keep = np.where(\n",
        "    stabilizer_group.x.any(axis=1) | stabilizer_group.z.any(axis=1)\n",
        ")[0]\n",
        "chosen = np.random.default_rng(seed).choice(\n",
        "    keep, size=min(num_stabilizers, len(keep)), replace=False\n",
        ")\n",
        "stabilizers = [stabilizer_group[int(i)] for i in chosen]\n",
        "\n",
        "two_qubit_depth = circuit.depth(lambda x: x.operation.num_qubits == 2)\n",
        "print(\n",
        "    f\"Sampled {len(stabilizers)} stabilizers of a {circuit.num_qubits}-qubit \"\n",
        "    f\"circuit with two-qubit depth {two_qubit_depth}: \"\n",
        "    f\"{{{stabilizers[0]}, {stabilizers[1]}, ...}}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step3-findchecks-md",
      "metadata": {},
      "source": [
        "Para cada estabilizador muestreado, gira el circuito de modo que el estabilizador se mida en la base computacional, transpílalo al backend y busca un buen conjunto de comprobaciones. Los pares de partícula objetivo y partícula auxiliar se mezclan para cada estabilizador, de modo que cada partícula objetivo conserve su partícula auxiliar. Recuerda que las comprobaciones se confirman de forma secuencial, siguiendo el orden en que se especifican los qubits de destino, y que una comprobación confirmada no se modifica aunque se añadan más comprobaciones.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "step3-findchecks",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 20/20 [00:15<00:00,  1.29it/s]"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Added 7 checks to 20 circuits in 15s.\n",
            "On average, two-qubit depth increased from 24 to 33 when adding 7 checks.\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "\n"
          ]
        }
      ],
      "source": [
        "noisy_circuits = []\n",
        "checked_circuits = []\n",
        "depths_2q = []\n",
        "t0 = time.time()\n",
        "for i, pauli in enumerate(tqdm(stabilizers)):\n",
        "    noisy_circuits.append(pm.run(append_basis_rotation(circuit, pauli)))\n",
        "    # Shuffle target and ancilla pairs together so each target keeps its ancilla\n",
        "    targets, ancillas = zip(\n",
        "        *random.sample(\n",
        "            list(zip(target_qubits, ancilla_qubits, strict=True)),\n",
        "            k=len(target_qubits),\n",
        "        ),\n",
        "        strict=True,\n",
        "    )\n",
        "    checked_circuits.append(\n",
        "        add_pauli_checks(\n",
        "            noisy_circuits[-1],\n",
        "            list(targets),\n",
        "            noise_model,\n",
        "            ancilla_qubits=list(ancillas),\n",
        "            cost=\"gamma\",\n",
        "            method=\"windowed\",\n",
        "            seed=seed + 1 + i,\n",
        "        )\n",
        "    )\n",
        "    depths_2q.append(\n",
        "        checked_circuits[-1][-1].circuit.depth(lambda x: len(x.qubits) == 2)\n",
        "    )\n",
        "\n",
        "print(\n",
        "    f\"Added {num_checks} checks to {len(stabilizers)} circuits \"\n",
        "    f\"in {(time.time() - t0):.0f}s.\"\n",
        ")\n",
        "print(\n",
        "    f\"On average, two-qubit depth increased from \"\n",
        "    f\"{circuit.depth(lambda x: len(x.qubits) == 2)} to {int(np.mean(depths_2q))} \"\n",
        "    f\"when adding {num_checks} checks.\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step3-sample-md",
      "metadata": {},
      "source": [
        "Prueba la carga útil básica y los circuitos comprobados con Qiskit Aer. El simulador utiliza el mismo modelo despolarizador que se empleó para evaluar las comprobaciones, por lo que el ruido al que se dirigen las comprobaciones es el mismo que aplica el simulador.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "step3-sample",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 20/20 [00:17<00:00,  1.13it/s]\n"
          ]
        }
      ],
      "source": [
        "aer_nm = AerNoiseModel()\n",
        "aer_nm.add_all_qubit_quantum_error(\n",
        "    depolarizing_error(noise_model.gate_noise, 2), [\"cz\"]\n",
        ")\n",
        "p = noise_model.readout_noise\n",
        "aer_nm.add_all_qubit_readout_error(ReadoutError([[1 - p, p], [p, 1 - p]]))\n",
        "noisy_sim = AerSimulator(method=\"stabilizer\", noise_model=aer_nm)\n",
        "\n",
        "counts = []\n",
        "for i, checked_circ_result in enumerate(tqdm(checked_circuits)):\n",
        "    noisy_counts = (\n",
        "        noisy_sim.run(\n",
        "            noisy_circuits[i], shots=num_shots, seed_simulator=seed * i + 1\n",
        "        )\n",
        "        .result()\n",
        "        .get_counts()\n",
        "    )\n",
        "    checked_counts_per_variant = []\n",
        "    for k, ck in enumerate(checked_circ_result):\n",
        "        variant_counts = (\n",
        "            noisy_sim.run(\n",
        "                ck.circuit, shots=num_shots, seed_simulator=seed * i + 2 + k\n",
        "            )\n",
        "            .result()\n",
        "            .get_counts()\n",
        "        )\n",
        "        checked_counts_per_variant.append(variant_counts)\n",
        "    counts.append((noisy_counts, checked_counts_per_variant))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step4-header",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Paso 4: Realizar el posprocesamiento y obtener el resultado en el formato clásico deseado\n",
        "\n",
        "Cada comprobación utiliza puertas de entrelazamiento entre una ancilla y un objetivo. La ancilla se inicia en $|0\\rangle$, por lo que $Z_\\text{anc}$ estabiliza su entrada. Al propagar un « $Z_\\text{anc}$ » hacia adelante a través del circuito comprobado, se obtiene un operador de Pauli en la salida cuyos términos distintos de la identidad definen el soporte de la comprobación. Un cheque se considera válido cuando los bits que lo componen tienen paridad par. Una muestra solo se conserva cuando supera todas las comprobaciones.\n",
        "\n",
        "La de `get_postselection_method` cada devuelve `CheckedCircuit` una función que asocia una cadena de bits medida a un vector de síndrome. Conserva las muestras cuyo síndrome sea cero en cada comprobación y descarta el resto. El gráfico que se muestra a continuación indica que aumentar el número de comprobaciones reduce la tasa de postselección. Una tasa de poselección más baja requiere un mayor número de disparos para alcanzar la precisión deseada, por lo que existe un equilibrio entre la capacidad de detección y el coste de muestreo. La tasa parece converger, lo que indica que las comprobaciones adicionales aportan una menor capacidad de detección.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "step4-psr",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step4-psr-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "rate_per_variant = []\n",
        "kept_per_stab = []\n",
        "for i, (_, checked_counts_per_variant) in enumerate(counts):\n",
        "    rates = []\n",
        "    kept_at_num_checks = None\n",
        "    for k, variant_counts in enumerate(checked_counts_per_variant):\n",
        "        ps_fn = checked_circuits[i][k].get_postselection_method()\n",
        "        kept = {\n",
        "            bs: n for bs, n in variant_counts.items() if not ps_fn(bs).any()\n",
        "        }\n",
        "        rates.append(sum(kept.values()) / num_shots)\n",
        "        if k == num_checks:\n",
        "            kept_at_num_checks = kept\n",
        "    rate_per_variant.append(rates)\n",
        "    kept_per_stab.append(kept_at_num_checks)\n",
        "\n",
        "max_len = max(len(s) for s in rate_per_variant)\n",
        "rates_arr = np.full((len(rate_per_variant), max_len), np.nan)\n",
        "for i, s in enumerate(rate_per_variant):\n",
        "    rates_arr[i, : len(s)] = s\n",
        "ks = np.arange(max_len)\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(8, 4))\n",
        "ax.plot(ks, rates_arr.T, color=\"#ff8c00\", alpha=0.15, linewidth=1)\n",
        "ax.plot(\n",
        "    ks,\n",
        "    np.nanmedian(rates_arr, axis=0),\n",
        "    color=\"black\",\n",
        "    linewidth=1,\n",
        "    linestyle=\"--\",\n",
        "    label=\"median\",\n",
        ")\n",
        "ax.set_xlabel(\"Checks committed\")\n",
        "ax.set_ylabel(\"Postselection rate\")\n",
        "ax.set_ylim((0, 1.05))\n",
        "ax.set_title(\n",
        "    f\"Per-stabilizer postselection rate ({len(rates_arr)} stabilizers)\"\n",
        ")\n",
        "ax.legend()\n",
        "ax.grid(True, alpha=0.3)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step4-fidelity-md",
      "metadata": {},
      "source": [
        "Ahora compara la fidelidad del estado «puro» con ruido con la del estado postseleccionado. La selección a posteriori de únicamente las muestras en las que no se ha detectado ningún error aumenta el valor esperado de cada estabilizador y, por lo tanto, la fidelidad estimada. Los valores poseleccionados utilizan menos muestras que los valores brutos; sin embargo, los valores esperados son más precisos y la varianza muestral es menor. Obsérvese también que la tasa media de postselección se aproxima a la fidelidad con ruido. Esto es lo que cabe esperar cuando las comprobaciones detectan casi todas las muestras erróneas: la proporción de muestras que superan todas las comprobaciones se aproxima a la proporción de muestras sin errores, que es la fidelidad del estado con ruido.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "step4-fidelity",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "ideal fidelity:          1.0\n",
            "noisy fidelity:          0.7899\n",
            "postselected fidelity:   0.9679\n",
            "mean postselection rate: 0.780\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step4-fidelity-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step4-fidelity-2.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "results = []\n",
        "for i, ((noisy_counts, _), kept) in enumerate(\n",
        "    zip(counts, kept_per_stab, strict=True)\n",
        "):\n",
        "    results.append(\n",
        "        (\n",
        "            expectation(noisy_counts, stabilizers[i]),\n",
        "            expectation(kept, stabilizers[i]),\n",
        "            sum(kept.values()) / num_shots,\n",
        "        )\n",
        "    )\n",
        "\n",
        "fidelity_noisy = float(np.nanmean([r[0] for r in results]))\n",
        "fidelity_postsel = float(np.nanmean([r[1] for r in results]))\n",
        "psr = float(np.mean([r[2] for r in results]))\n",
        "print(\n",
        "    f\"ideal fidelity:          1.0\\n\"\n",
        "    f\"noisy fidelity:          {fidelity_noisy:.4f}\\n\"\n",
        "    f\"postselected fidelity:   {fidelity_postsel:.4f}\\n\"\n",
        "    f\"mean postselection rate: {psr:.3f}\"\n",
        ")\n",
        "\n",
        "evs_ideal = np.ones(len(results))\n",
        "evs_noisy = np.array([r[0] for r in results])\n",
        "evs_post = np.array([r[1] for r in results])\n",
        "idx = np.arange(len(results))\n",
        "\n",
        "\n",
        "def strip(ax, ys, color, label):\n",
        "    m, s = np.nanmean(ys), np.nanstd(ys)\n",
        "    ax.axhspan(\n",
        "        m - s, m + s, color=color, alpha=0.15, label=f\"{label} mean and std\"\n",
        "    )\n",
        "    ax.axhline(\n",
        "        m, color=color, linewidth=1, linestyle=\"--\", label=f\"{label} fidelity\"\n",
        "    )\n",
        "\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(8, 4))\n",
        "ax.axhline(np.nanmean(evs_ideal), color=\"black\", linewidth=1.5, label=\"ideal\")\n",
        "strip(ax, evs_noisy, \"red\", \"noisy\")\n",
        "strip(ax, evs_post, \"green\", \"postselected\")\n",
        "ax.scatter(idx, evs_noisy, color=\"red\", s=22, alpha=0.7, label=\"noisy EVs\")\n",
        "ax.scatter(\n",
        "    idx,\n",
        "    evs_post,\n",
        "    color=\"green\",\n",
        "    s=22,\n",
        "    alpha=0.7,\n",
        "    label=\"postselected EVs\",\n",
        ")\n",
        "ax.set_xlabel(\"stabilizer index\")\n",
        "ax.set_ylabel(r\"$\\langle G \\rangle$\")\n",
        "ax.set_ylim((-0.1, 1.1))\n",
        "ax.set_title(\"Per-stabilizer expectation values\")\n",
        "ax.legend(loc=\"lower left\")\n",
        "ax.grid(True, alpha=0.3)\n",
        "plt.tight_layout()\n",
        "plt.show()\n",
        "\n",
        "M = np.arange(1, len(results) + 1)\n",
        "fig, ax = plt.subplots(figsize=(8, 4))\n",
        "for ys, color, label in [\n",
        "    (evs_ideal, \"black\", \"ideal\"),\n",
        "    (evs_noisy, \"red\", \"noisy\"),\n",
        "    (evs_post, \"green\", \"postselected\"),\n",
        "]:\n",
        "    cm, sem = cum_mean_sem(ys)\n",
        "    ax.plot(M, cm, color=color, linewidth=1.5, label=label)\n",
        "    ax.fill_between(M, cm - sem, cm + sem, color=color, alpha=0.15)\n",
        "ax.set_xlabel(\"number of stabilizers averaged\")\n",
        "ax.set_ylabel(\"running fidelity estimate\")\n",
        "ax.set_title(\"Fidelity convergence versus number of stabilizers\")\n",
        "ax.legend()\n",
        "ax.grid(True, alpha=0.3)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step4-gamma-md",
      "metadata": {},
      "source": [
        "La puntuación gamma indica qué parte del canal de ruido modelado no ha sido detectada por los controles. Al representar gráficamente la puntuación gamma en función del número de comprobaciones realizadas, se observa cómo mejora la capacidad de detección a medida que se añade cada comprobación. Un valor de significa `1.0` que las comprobaciones captan todo el ruido modelado. Las curvas descienden hacia `1.0` a medida que se realizan más comprobaciones, lo que demuestra que cada comprobación adicional detecta parte del error restante que aún no se ha detectado.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "step4-gamma",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/step4-gamma-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "stab_scores = [\n",
        "    [variant.cost for variant in checked_circ_result]\n",
        "    for checked_circ_result in checked_circuits\n",
        "]\n",
        "max_len = max(len(s) for s in stab_scores)\n",
        "scores = np.full((len(stab_scores), max_len), np.nan)\n",
        "for i, s in enumerate(stab_scores):\n",
        "    scores[i, : len(s)] = s\n",
        "ks = np.arange(max_len)\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(8, 4))\n",
        "ax.plot(ks, scores.T, color=\"#4682b4\", alpha=0.15, linewidth=1)\n",
        "ax.plot(\n",
        "    ks,\n",
        "    np.nanmedian(scores, axis=0),\n",
        "    color=\"black\",\n",
        "    linewidth=1,\n",
        "    linestyle=\"--\",\n",
        "    label=\"median\",\n",
        ")\n",
        "ax.set_xlabel(\"Checks committed\")\n",
        "ax.set_ylabel(\"Gamma\")\n",
        "ax.set_yscale(\"log\")\n",
        "ax.set_title(f\"Per-stabilizer gamma curves ({len(scores)} stabilizers)\")\n",
        "ax.legend()\n",
        "ax.grid(True, alpha=0.3, which=\"both\")\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "hardware-header",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Ejemplo de hardware a gran escala\n",
        "\n",
        "El mismo flujo de trabajo se ejecuta en un equipo con una capacidad de carga útil mayor y más profunda. En esta sección se reutiliza el backend del ejemplo del simulador, pero se crea una nueva configuración de 20 qubits con sus propios pares de qubits objetivo y auxiliares, así como su propio gestor de pasadas; a continuación, se envían los circuitos a la QPU en un único trabajo. Con este tamaño, la mayoría de los disparos activan al menos una comprobación, por lo que la tasa de poselección es baja, y cada circuito necesita un presupuesto de disparos elevado para obtener muestras suficientes que permitan su supervivencia. Por lo tanto, el ejemplo centra su presupuesto en unos pocos estabilizadores seleccionados al azar; se trata, no obstante, de una estimación insesgada de la fidelidad, aunque es menos precisa que la media calculada en el ejemplo del simulador a partir de numerosos estabilizadores.\n",
        "\n",
        "Hay un cambio respecto al ejemplo del simulador: en lugar de inferir un canal despolarizador uniforme a partir de los datos de calibración, en esta sección **se aprende** el modelo de ruido con `NoiseLearner` y se construye el modelo `qiskit-paulice` a partir del resultado con `NoiseModel.from_pauli_lindblad_maps`. Un modelo de Pauli-Lindblad entrenado reproduce la estructura espacial del ruido en esta disposición concreta, en lugar de suponer que todos los bordes presentan el mismo nivel de ruido, por lo que la colocación de los cheques se evalúa en función de un ruido más similar al que afecta a la QPU. Para conocer el ruido es necesario realizar un muestreo de la QPU, lo cual debe tenerse en cuenta en cualquier presupuesto global de muestreo de la QPU.\n",
        "\n",
        "Los parámetros que figuran a continuación establecen el número de qubits, la profundidad, el número de estabilizadores y el número de disparos. Escala `hw_num_shots` con la inversa de la tasa de poselección: con una tasa del 3 %, de 40 000 disparos quedan aproximadamente 1 200 muestras poseleccionadas por circuito. Aumenta `hw_num_stabilizers` para obtener una estimación de la fidelidad más precisa, a costa de un mayor número de circuitos por trabajo, cada uno de los cuales requiere el mismo presupuesto de disparos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "hardware-steps-header",
      "metadata": {},
      "source": [
        "<span id=\"steps-1-4-compressed-into-a-single-code-block\" />\n",
        "\n",
        "### Pasos 1-4 (comprimidos en un único bloque de código)\n",
        "\n",
        "La siguiente celda ejecuta los mismos cuatro pasos que el ejemplo del simulador. Crea la carga útil más grande y toma muestras de varios estabilizadores (paso 1); elige una disposición, aprende el modelo de ruido en ella y encuentra el circuito totalmente comprobado para cada estabilizador (paso 2); envía un trabajo de Sampler que contiene tanto los circuitos básicos como los comprobados (paso 3); y realiza una poselección de los recuentos comprobados para comparar las estimaciones de fidelidad con ruido y las poseleccionadas, por estabilizador y de media (paso 4). Con este tamaño, enumerar todo el grupo de estabilizadores, como en el ejemplo del simulador, resulta inviable, por lo que la celda toma una submuestra aleatoria de estabilizadores para calcular una estimación de la fidelidad.\n",
        "\n",
        "Ten en cuenta que el paso 2 realiza más acciones aquí que en el ejemplo del simulador: el aprendizaje del modelo de ruido envía su propio `NoiseLearner` trabajo antes que el trabajo del muestreador, por lo que la celda ejecuta **dos** trabajos en total. Llevan las etiquetas `TUT_ASPC_LEARN` y `TUT_ASPC` para que puedas encontrarlos más tarde. Consulta [«Organizar y buscar por etiquetas de ofertas de empleo»](/docs/guides/add-job-tags) para obtener más información sobre cómo etiquetar las ofertas de empleo.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "hardware-submit",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Layout with 11 check pairs: [153, 152, 151, 138, 131, 130, 129, 118, 109, 110, 111, 98, 91, 90, 89, 78, 69, 70, 71, 58]\n",
            "Submitted noise-learner job d9f4mncjeosc73fjfmkg\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Learned 4 layers; readout noise 0.00470\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "100%|██████████| 10/10 [01:01<00:00,  6.18s/it]\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Submitted job d9f4s04jeosc73fjftkg with 20 circuits\n",
            "noisy fidelity estimate:        0.3685\n",
            "postselected fidelity estimate: 0.6869\n",
            "mean postselection rate:        0.2851 (~11404 kept shots per circuit)\n",
            "\n",
            "per-stabilizer results:\n",
            "idx     noisy   postsel      psr  kept shots\n",
            "  0    0.3769    0.6918   0.3247       12987\n",
            "  1    0.3745    0.6760   0.2999       11995\n",
            "  2    0.3659    0.6389   0.3549       14196\n",
            "  3    0.3821    0.7060   0.2660       10641\n",
            "  4    0.3653    0.7475   0.2531       10124\n",
            "  5    0.3752    0.7022   0.2698       10791\n",
            "  6    0.3508    0.7144   0.2711       10842\n",
            "  7    0.3485    0.7087   0.2381        9523\n",
            "  8    0.3825    0.6289   0.2928       11711\n",
            "  9    0.3630    0.6549   0.2808       11232\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/spacetime-codes/extracted-outputs/hardware-submit-5.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# -------------------------Step 1: build a larger payload and sample stabilizers-------------------------\n",
        "hw_num_qubits = 20\n",
        "hw_depth = 36\n",
        "hw_num_stabilizers = 10\n",
        "hw_num_shots = 40_000\n",
        "\n",
        "hw_circuit = random_clifford_circuit(hw_num_qubits, hw_depth, rng)\n",
        "hw_no_meas = hw_circuit.remove_final_measurements(inplace=False)\n",
        "\n",
        "# Enumerating all 2^n stabilizers is infeasible at this size, so draw each\n",
        "# stabilizer by composing a random subset of the group generators\n",
        "hw_generators = [\n",
        "    Pauli(label) for label in Clifford(hw_no_meas).to_labels(mode=\"S\")\n",
        "]\n",
        "sample_rng = np.random.default_rng(seed)\n",
        "hw_stabilizers = []\n",
        "while len(hw_stabilizers) < hw_num_stabilizers:\n",
        "    mask = sample_rng.integers(0, 2, hw_num_qubits).astype(bool)\n",
        "    if not mask.any():\n",
        "        continue  # skip the identity\n",
        "    stabilizer = Pauli(\"I\" * hw_num_qubits)\n",
        "    for generator, chosen in zip(hw_generators, mask, strict=True):\n",
        "        if chosen:\n",
        "            stabilizer = stabilizer.compose(generator)\n",
        "    hw_stabilizers.append(stabilizer)\n",
        "\n",
        "# -------------------------Step 2: find a 20-qubit layout, learn its noise, and add checks-------------------------\n",
        "# A single bad coupler or bad-readout qubit on the path drags every\n",
        "# stabilizer down, so search harder and with tighter error thresholds\n",
        "hw_layout = find_check_layout(\n",
        "    backend,\n",
        "    hw_num_qubits,\n",
        "    rng,\n",
        "    num_trials=500,\n",
        "    max_gate_error=0.015,\n",
        "    max_readout_error=0.05,\n",
        ")\n",
        "hw_target_qubits, hw_ancilla_qubits = get_check_qubits(backend, hw_layout)\n",
        "hw_pm = generate_preset_pass_manager(\n",
        "    optimization_level=0, backend=backend, initial_layout=hw_layout\n",
        ")\n",
        "print(f\"Layout with {len(hw_target_qubits)} check pairs: {hw_layout}\")\n",
        "\n",
        "# ----- learn a Pauli-Lindblad noise model on this layout -----\n",
        "# The simulator example scored checks against a uniform depolarizing channel\n",
        "# inferred from calibration data. Here, learn the noise instead: NoiseLearner\n",
        "# runs its own job on the QPU and returns a Pauli-Lindblad channel per unique\n",
        "# entangling layer, so the checks are placed against the noise this layout\n",
        "# actually has, including its spatial structure. All the sampled stabilizers\n",
        "# share the same entangling layers and differ only in their final basis\n",
        "# rotation, so learning on the bare payload covers all of them.\n",
        "learner = NoiseLearner(\n",
        "    mode=backend,\n",
        "    options={\n",
        "        \"max_layers_to_learn\": 4,\n",
        "        \"num_randomizations\": 32,\n",
        "        \"shots_per_randomization\": 128,\n",
        "        \"environment\": {\"job_tags\": [\"TUT_ASPC_LEARN\"]},\n",
        "    },\n",
        ")\n",
        "learner_job = learner.run([hw_pm.run(hw_circuit)])\n",
        "print(f\"Submitted noise-learner job {learner_job.job_id()}\")\n",
        "hw_layer_errors = learner_job.result().data\n",
        "\n",
        "# To see how much the learned model helps, swap the next line for the\n",
        "# simulator example's uniform model - a one-line change:\n",
        "# hw_noise_model = NoiseModel.from_backend(backend, hw_layout, uniform_gate_noise=True)\n",
        "hw_noise_model = learned_noise_model(hw_layer_errors, hw_layout)\n",
        "# NoiseLearner characterizes gate noise only, so keep the readout estimate\n",
        "# from calibration data rather than leaving it unset\n",
        "hw_noise_model.readout_noise = NoiseModel.from_backend(\n",
        "    backend, hw_layout, uniform_gate_noise=True\n",
        ").readout_noise\n",
        "print(\n",
        "    f\"Learned {len(hw_layer_errors)} layers; \"\n",
        "    f\"readout noise {hw_noise_model.readout_noise:.5f}\"\n",
        ")\n",
        "\n",
        "# ----- add the fully checked circuit per stabilizer -----\n",
        "hw_noisy_circuits = []\n",
        "hw_checked_circuits = []\n",
        "for i, pauli in enumerate(tqdm(hw_stabilizers)):\n",
        "    bare = hw_pm.run(append_basis_rotation(hw_circuit, pauli))\n",
        "    hw_noisy_circuits.append(bare)\n",
        "    variants = add_pauli_checks(\n",
        "        bare,\n",
        "        hw_target_qubits,\n",
        "        hw_noise_model,\n",
        "        ancilla_qubits=hw_ancilla_qubits,\n",
        "        cost=\"gamma\",\n",
        "        method=\"windowed\",\n",
        "        seed=seed + 1 + i,\n",
        "    )\n",
        "    hw_checked_circuits.append(variants[-1])  # keep the fully checked circuit\n",
        "\n",
        "# -------------------------Step 3: submit one Sampler job with the bare and checked circuits-------------------------\n",
        "sampler = Sampler(mode=backend)\n",
        "sampler.options.default_shots = hw_num_shots\n",
        "sampler.options.environment.job_tags = [\"TUT_ASPC\"]\n",
        "\n",
        "pubs = hw_noisy_circuits + [cc.circuit for cc in hw_checked_circuits]\n",
        "job = sampler.run(pubs)\n",
        "print(f\"Submitted job {job.job_id()} with {len(pubs)} circuits\")\n",
        "\n",
        "# -------------------------Step 4: postselect and compare fidelity-------------------------\n",
        "result = job.result()\n",
        "n_stab = len(hw_stabilizers)\n",
        "\n",
        "hw_results = []\n",
        "for i in range(n_stab):\n",
        "    noisy_counts = result[i].join_data().get_counts()\n",
        "    checked_counts = result[n_stab + i].join_data().get_counts()\n",
        "    ps_fn = hw_checked_circuits[i].get_postselection_method()\n",
        "    kept = {bs: c for bs, c in checked_counts.items() if not ps_fn(bs).any()}\n",
        "    hw_results.append(\n",
        "        (\n",
        "            expectation(noisy_counts, hw_stabilizers[i]),\n",
        "            expectation(kept, hw_stabilizers[i]),\n",
        "            sum(kept.values()) / sum(checked_counts.values()),\n",
        "        )\n",
        "    )\n",
        "\n",
        "hw_fidelity_noisy = float(np.nanmean([r[0] for r in hw_results]))\n",
        "hw_fidelity_postsel = float(np.nanmean([r[1] for r in hw_results]))\n",
        "hw_psr = float(np.mean([r[2] for r in hw_results]))\n",
        "print(\n",
        "    f\"noisy fidelity estimate:        {hw_fidelity_noisy:.4f}\\n\"\n",
        "    f\"postselected fidelity estimate: {hw_fidelity_postsel:.4f}\\n\"\n",
        "    f\"mean postselection rate:        {hw_psr:.4f} \"\n",
        "    f\"(~{int(round(hw_psr * hw_num_shots))} kept shots per circuit)\"\n",
        ")\n",
        "\n",
        "# Per-stabilizer breakdown. The postselection rate varies from stabilizer to\n",
        "# stabilizer, so a stabilizer whose postselected value barely moves is usually\n",
        "# one whose checks rejected little; the kept-shot count says how much of the\n",
        "# gap is statistics rather than signal.\n",
        "print(\"\\nper-stabilizer results:\")\n",
        "print(\n",
        "    f\"{'idx':>3}  {'noisy':>8}  {'postsel':>8}  {'psr':>7}  {'kept shots':>10}\"\n",
        ")\n",
        "for i, (noisy, post, psr_i) in enumerate(hw_results):\n",
        "    print(\n",
        "        f\"{i:>3}  {noisy:>8.4f}  {post:>8.4f}  {psr_i:>7.4f}  \"\n",
        "        f\"{int(round(psr_i * hw_num_shots)):>10}\"\n",
        "    )\n",
        "\n",
        "hw_noisy = np.array([r[0] for r in hw_results])\n",
        "hw_post = np.array([r[1] for r in hw_results])\n",
        "idx = np.arange(n_stab)\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(9, 4))\n",
        "ax.axhline(1.0, color=\"black\", linewidth=1.5, label=\"ideal\")\n",
        "strip(ax, hw_noisy, \"red\", \"noisy\")\n",
        "strip(ax, hw_post, \"green\", \"postselected\")\n",
        "ax.scatter(idx, hw_noisy, color=\"red\", s=22, alpha=0.7, label=\"noisy EVs\")\n",
        "ax.scatter(\n",
        "    idx,\n",
        "    hw_post,\n",
        "    color=\"green\",\n",
        "    s=22,\n",
        "    alpha=0.7,\n",
        "    label=\"postselected EVs\",\n",
        ")\n",
        "ax.set_xlabel(\"stabilizer index\")\n",
        "ax.set_ylabel(r\"$\\langle G \\rangle$\")\n",
        "ax.set_ylim((-0.1, 1.1))\n",
        "ax.set_xticks(idx)\n",
        "ax.set_title(\"Per-stabilizer expectation values on hardware\")\n",
        "# Outside the axes so it cannot hide a data point\n",
        "ax.legend(loc=\"center left\", bbox_to_anchor=(1.02, 0.5), frameon=False)\n",
        "ax.grid(True, alpha=0.3)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "hardware-results-md",
      "metadata": {},
      "source": [
        "En circuitos de este tamaño, la mayoría de las muestras contienen al menos un error detectado, por lo que la tasa de postselección es baja y la postselección descarta la mayoría de las tomas. Las muestras que superan todas las comprobaciones ofrecen un valor esperado mucho mejor que el del circuito sin más, y los valores por estabilizador se distinguen claramente de la línea de base con ruido. Para mejorar la precisión de la estimación, toma más muestras de estabilizadores con el mismo presupuesto de disparos por circuito. Para aumentar la tasa de postselección, reduce la profundidad del circuito o realiza menos comprobaciones; para conseguir cargas útiles mayores, adapta el presupuesto de disparos a la inversa de la tasa de postselección.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "next-steps",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Próximos pasos\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendaciones\">\n",
        "  Si este trabajo te ha parecido interesante, quizá te interese el siguiente material:\n",
        "\n",
        "  * El tutorial sobre [códigos de repetición,](/docs/tutorials/repetition-codes) como introducción a la corrección de errores cuánticos.\n",
        "  * La [documentación `qiskit-paulice`](/docs/addons/qiskit-addon-paulice) de la API completa de detección de cheques y el [repositorio GitHub](https://github.com/Qiskit/qiskit-paulice) del paquete, donde se encuentra el código fuente.\n",
        "  * El artículo [«Detección de errores con baja sobrecarga mediante códigos espaciotemporales»,](https://arxiv.org/abs/2504.15725) que explica la teoría en la que se basan las comprobaciones.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "references",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Referencias\n",
        "\n",
        "* \\[1] Martiel, S., y Javadi-Abhari, A. (2025). Detección de errores con baja sobrecarga mediante códigos espaciotemporales. *arXiv Preimpresión* [arXiv:2504.15725](https://arxiv.org/abs/2504.15725).\n",
        "* \\[2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., y Temme, K. (2023). Mitigación de errores en un solo disparo mediante comprobaciones de Pauli coherentes. *Physical Review Research*, 5(3), 033193.\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": 2.5,
    "qpuSeconds": 10
  },
  "nbformat": 4,
  "nbformat_minor": 5
}