{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "frontmatter",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Detecção de erros com baixa sobrecarga usando códigos espaço-temporais\"\n",
        "description: \"Insira automaticamente verificações de Pauli no espaço-tempo com o qiskit-paulice e aumente a fidelidade dos circuitos de Clifford amostrados por meio da pós-seleção com base em síndromes de verificação.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore postsel, frameon, virt */}\n",
        "\n",
        "<span id=\"low-overhead-error-detection-with-spacetime-codes\" />\n",
        "\n",
        "# Detecção de erros com baixa sobrecarga usando códigos espaço-temporais\n",
        "\n",
        "Estimativa de *tempo de execução: 4 minutos em um processador Heron (`ibm_kingston` ou equivalente) (NOTA: Trata-se apenas de uma estimativa.) (O tempo de execução pode variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "learning-outcomes",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados do aprendizado\n",
        "\n",
        "* Como as verificações de Pauli no espaço-tempo detectam erros lógicos em circuitos de Clifford e como a pós-seleção com base em suas síndromes aumenta a fidelidade de uma distribuição amostrada.\n",
        "* Como usar o pacote `qiskit-paulice` para localizar e inserir verificações eficientes em termos de hardware automaticamente com `get_check_qubits`, `NoiseModel`, e `add_pauli_checks`.\n",
        "* Como estimar a fidelidade de um estado estabilizador por meio da amostragem de seus estabilizadores e da pós-seleção com base em síndromes de verificação.\n",
        "* Como executar o fluxo de trabalho completo de detecção de erros em um hardware d IBM Quantum® e e comparar as fidelidades com ruído e pós-selecionadas.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "prerequisites",
      "metadata": {},
      "source": [
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Pré-requisitos\n",
        "\n",
        "* [Fundamentos de hardware](/learning/courses/utility-scale-quantum-computing/hardware) para computação quântica em escala de serviços públicos.\n",
        "* O formalismo de Clifford e do estabilizador, incluindo como um grupo estabilizador descreve um estado puro do estabilizador.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "background",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Segundo plano\n",
        "\n",
        "[O artigo “Detecção de erros com baixa sobrecarga usando códigos espaço-temporais”](https://arxiv.org/abs/2504.15725) [\\[1\\],](#references) de Simon Martiel e Ali Javadi-Abhari, apresenta um método para detectar erros lógicos em circuitos dominados por Clifford que se situa entre a correção completa de erros e a mitigação de erros, que é uma abordagem mais leve. A ideia se baseia nas verificações coerentes de Pauli (CPC) descritas no artigo [“Mitigação de erros em um único disparo por meio de verificações coerentes de Pauli”](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.5.033193) [\\[2\\],](#references) de van den Berg e outros. Em ambas as abordagens, um circuito de “carga útil” de Clifford é entrelaçado com qubits auxiliares para verificar certas invariantes. A medição das ancillas gera uma síndrome que indica se foi detectado algum erro durante a execução. Manter apenas as amostras nas quais não foi detectado nenhum erro melhora a fidelidade da distribuição amostrada, em troca de uma taxa de pós-seleção reduzida.\n",
        "\n",
        "A principal diferença entre as verificações coerentes de Pauli e as verificações no espaço-tempo é os operadores que elas medem. As verificações de Pauli coerentes medem operadores localizados no tempo e de alto peso. Em topologias de qubits com conectividade limitada, como a “heavy hex”, essas verificações exigem muitas portas SWAP e, muitas vezes, tornam o circuito muito complexo para ser executado na prática. A implementação das verificações como códigos espaço-temporais, por outro lado, distribui cada verificação pelo circuito de carga útil no espaço e no tempo. Isso resulta em uma codificação eficiente em termos de hardware que mantém a eficácia na detecção de erros lógicos, ao mesmo tempo em que mantém baixas as sobrecargas relacionadas aos qubits e à profundidade.\n",
        "\n",
        "<span id=\"what-the-qiskit-paulice-package-does\" />\n",
        "\n",
        "### O que o pacote `qiskit-paulice` faz\n",
        "\n",
        "O pacote `qiskit-paulice` automatiza a criação dessas verificações, para que você não precise criá-las manualmente. Sua principal função é identificar e inserir verificações de Pauli válidas no espaço-tempo nos pontos de um circuito que maximizem a detecção de erros e, ao mesmo tempo, minimizem a sobrecarga dos qubits. Uma verificação é *válida* quando seus operadores não alteram a ação lógica do circuito de carga útil, *é de baixo peso* quando utiliza poucas portas de entrelaçamento e *é* eficaz quando detecta uma grande parcela dos erros, em relação ao ruído que a própria verificação introduz. O pacote avalia as verificações candidatas em relação a um modelo de ruído e incorpora as melhores ao circuito. Este tutorial utiliza três métodos da API:\n",
        "\n",
        "* `get_check_qubits` inspeciona um mapa de acoplamento do backend e retorna pares de qubits-alvo e qubits auxiliares. Uma verificação sobre utiliza `target_qubits[i]` `ancilla_qubits[i]`.\n",
        "* `NoiseModel.from_backend` cria um modelo aproximado de ruído a partir de dados de benchmark do backend. O modelo avalia as verificações dos candidatos; portanto, não é necessário um modelo de ruído exato e treinado. Para um modelo de Pauli-Lindblad treinado, consulte [`NoiseModel.from_pauli_lindblad_maps`](/docs/api/qiskit-addon-paulice/qiskit-paulice-noise-models#from_pauli_lindblad_maps).\n",
        "* `add_pauli_checks` localiza e insere verificações em um circuito. Ela retorna uma sequência de `CheckedCircuit` objetos com um número crescente de verificações, e cada objeto fornece uma função `get_postselection_method` que mapeia uma cadeia de bits medida para um vetor de síndrome. O argumento `cost` seleciona a função que avalia um teste (`gamma`, a sobrecarga de amostragem do canal de ruído inverso pós-selecionado, ou `LER`, a taxa de erro lógico). O argumento `method` seleciona a estratégia de pesquisa (`windowed`, `genetic`, ou `windowed_genetic`). Este tutorial utiliza e `cost=\"gamma\"` `method=\"windowed\"`, que, juntos, proporcionam uma seleção de verificações determinística e reproduzível.\n",
        "\n",
        "<span id=\"estimate-fidelity-from-stabilizer-sampling\" />\n",
        "\n",
        "### Estimativa da fidelidade a partir da amostragem do estabilizador\n",
        "\n",
        "Para avaliar a eficácia da detecção de erros, é possível estimar a fidelidade entre o estado do estabilizador $|\\psi\\rangle = U|0\\rangle^{\\otimes n}$ — que o circuito, idealmente, prepara — e o estado com ruído $\\rho$ — que o hardware realmente gera. A projeção de um estado em um estabilizador puro $|\\psi\\rangle$ é igual à média uniforme sobre os elementos $2^n$ de seu grupo estabilizador $\\mathcal{S}$ :\n",
        "\n",
        "$|\\psi\\rangle\\langle\\psi| = \\frac{1}{2^n}\\sum_{G \\in \\mathcal{S}} G.$\n",
        "\n",
        "Substituindo isso na fórmula da fidelidade, obtém-se a fidelidade de $\\rho$ como o valor esperado médio de cada estabilizador $G \\in \\mathcal{S}$ em relação 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",
        "Para problemas maiores, enumerar todos os estabilizadores $2^n$ é inviável; portanto, é possível estimar a fidelidade a partir de uma amostra aleatória. A seleção de estabilizadores $M$ e $G_1, \\ldots, G_M$ de forma aleatória e uniforme a partir de $\\mathcal{S}$ fornece uma estimativa não sesgada:\n",
        "\n",
        "$\\hat F_M = \\frac{1}{M} \\sum_{i=1}^M \\langle G_i \\rangle_\\rho.$\n",
        "\n",
        "Como um circuito de Clifford prepara um estado estabilizador, é possível estimar sua fidelidade diretamente a partir dos valores esperados amostrados de seus estabilizadores. Este tutorial primeiro apresenta o fluxo de trabalho em um simulador com um circuito pequeno e, em seguida, executa o mesmo fluxo de trabalho em hardware com um circuito maior e mais complexo. À medida que os circuitos incluem mais operações não-Clifford, o número de verificações válidas diminui rapidamente; portanto, o método funciona melhor para circuitos dominados por operações Clifford.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "requirements",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de iniciar este tutorial, certifique-se de ter os seguintes itens instalados:\n",
        "\n",
        "* Qiskit SDK v2.0 ou versão posterior, com suporte [à visualização](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.40 ou posterior (`pip install qiskit-ibm-runtime`)\n",
        "* Qiskit Aer v0.17 ou versã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",
        "## Instalação\n",
        "\n",
        "Importe as bibliotecas necessárias e defina as funções auxiliares que não estão disponíveis como importações. A função `random_clifford_circuit` constrói uma carga útil Clifford aleatória do tipo “brickwork”, `find_check_layout` pesquisa um mapa de acoplamento do backend em busca de um caminho de qubits com baixo erro e muitas ancillas disponíveis, `learned_noise_model` transforma a saída `NoiseLearner` em um modelo de ruído `qiskit-paulice` , `append_basis_rotation` gira um circuito de modo que um estabilizador seja medido na base computacional, `expectation` calcula o valor esperado do estabilizador a partir de contagens amostradas e `cum_mean_sem` acompanha a estimativa de fidelidade em tempo 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",
        "## Exemplo de simulador em pequena escala\n",
        "\n",
        "Esta seção apresenta passo a passo todo o fluxo de trabalho em um simulador com ruído. Ele utiliza dados de benchmark do backend para escolher um layout de qubits e um modelo de ruído, identifica verificações automaticamente e utiliza pós-seleção na distribuição amostrada para demonstrar a melhoria na fidelidade.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "step1-header",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Etapa 1: Mapeie entradas clássicas para um problema quântico\n",
        "\n",
        "O circuito de carga útil é um circuito de Clifford aleatório unidimensional de estrutura em tijolo e pouca profundidade. Como o circuito é do tipo Clifford, ele prepara um estado estabilizador cuja fidelidade pode ser estimada diretamente a partir dos valores esperados amostrados do estabilizador. Comece com um circuito simples, para que as verificações sejam fáceis de visualizar na próxima etapa.\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",
        "### Etapa 2: Otimizar para execução em hardware quântico\n",
        "\n",
        "O mapeamento do circuito para o hardware define o layout físico dos qubits, o modelo de ruído que avalia as verificações candidatas e as próprias verificações.\n",
        "\n",
        "Primeiro, selecione um backend e procure em seu mapa de acoplamento um layout de qubits unidimensional com o `find_check_layout` auxiliar definido na seção “Configuração”. O auxiliar gera percursos aleatórios que evitam os portões e as leituras com maior erro, e mantém o caminho que oferece o maior número de pares de alvo e ancilla. Como a pesquisa lê os dados de conectividade e de erros diretamente do próprio backend, o mesmo código é executado em qualquer QPU do IBM Quantum. A função `get_check_qubits` retorna, então, os pares de alvo e ancilla, em que uma verificação sobre utiliza `target_qubits[i]` `ancilla_qubits[i]`.\n",
        "\n",
        "No gráfico de acoplamento a seguir, os qubits verdes são os qubits de carga útil e os qubits laranja são os qubits auxiliares que realizam as verificações. Os qubits com um ancilla adjacente são utilizados como qubits-alvo para as verificações.\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": [
        "Com o backend e o layout escolhidos, compile a carga útil em um circuito de arquitetura de conjunto de instruções (ISA). Basta definir o layout e converter os portões para o conjunto de portões nativo do 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": [
        "Em seguida, modele como o ruído de porta e de leitura no backend afetam a execução. O modelo de ruído determina em que ponto do circuito uma verificação detecta o maior número de erros. Um modelo mais preciso melhora a detecção, mas geralmente não é necessário treiná-lo por meio de amostragem na QPU. O modelo a seguir infere um canal despolarizador uniforme para o ruído de porta e de leitura a partir de dados de referência `qiskit-ibm-runtime` .\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": [
        "Agora, adicione os componentes de verificação ao circuito. A função `add_pauli_checks` recebe a carga útil de Clifford, a lista de qubits-alvo e o modelo de ruído. O argumento `ancilla_qubits` indica à função qual ancila física deve ser emparelhada com cada alvo. As verificações são adicionadas na ordem em que os qubits-alvo aparecem; portanto, o layout final do circuito verificado é `layout + ancilla_qubits`. Para operar um circuito de saída com menos (`i`) verificações, o layout final é `layout + ancilla_qubits[:i]`.\n",
        "\n",
        "A saída de é `add_pauli_checks` uma sequência de circuitos com um número crescente de verificações, desde nenhuma verificação até uma verificação em cada qubit-alvo. A visualização confirma que as verificações utilizam os pares de alvo e ancilla especificados. Para obter detalhes sobre como identificar bons testes, consulte as Seções II a IV das informações complementares na referência [\\[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",
        "### Etapa 3: Executar usando Qiskit primitives\n",
        "\n",
        "Para tornar visível o efeito do ruído da porta, aumente a profundidade da carga útil e amostre um subconjunto de seus estabilizadores. Geralmente, cada estabilizador não comuta com os outros em nível de qubit; portanto, um único conjunto de verificações não é válido para dois estabilizadores diferentes. Em vez de agrupar os estabilizadores em conjuntos de comutação, encontre um bom conjunto de verificações para cada estabilizador individualmente. A amostragem aleatória e uniforme dos estabilizadores fornece uma estimativa não sesgada da fidelidade.\n",
        "\n",
        "Construa o circuito mais complexo e selecione uma amostra aleatória de seus 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 amostrado, gire o circuito de modo que o estabilizador seja medido na base computacional, transpilá-lo para o backend e encontre um bom conjunto de verificações. Os pares de alvo e ancilla são embaralhados juntos para cada estabilizador, de modo que cada alvo mantenha sua ancilla. Lembre-se de que as verificações são confirmadas sequencialmente, na ordem em que os qubits-alvo são especificados, e uma verificação confirmada não é alterada à medida que mais verificações são adicionadas.\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": [
        "Teste a carga útil básica e os circuitos verificados com o Qiskit Aer. O simulador utiliza o mesmo modelo de despolarização usado para avaliar os testes; portanto, o ruído que os testes visam detectar é o mesmo ruído que o simulador aplica.\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",
        "### Etapa 4: Realizar o pós-processamento e apresentar o resultado no formato clássico desejado\n",
        "\n",
        "Cada verificação utiliza portas de entrelaçamento entre uma ancilla e um alvo. A ancilla é iniciada em $|0\\rangle$, portanto, $Z_\\text{anc}$ estabiliza sua entrada. A propagação de um $Z_\\text{anc}$ no sentido direto através do circuito verificado resulta em um operador de Pauli na saída, cujos termos diferentes da identidade definem o suporte da verificação. Um cheque é aprovado quando os bits que o compõem têm paridade par. Uma amostra só é mantida quando todas as verificações são aprovadas.\n",
        "\n",
        "O de `get_postselection_method` cada retorna `CheckedCircuit` uma função que mapeia uma sequência de bits medida para um vetor de síndrome. Mantenha as amostras cujo índice seja zero em cada verificação e descarte as demais. O gráfico a seguir mostra que aumentar o número de verificações reduz a taxa de pós-seleção. Uma taxa de pós-seleção mais baixa exige mais tentativas para atingir a precisão desejada; portanto, há um compromisso entre a capacidade de detecção e o custo de amostragem. A taxa parece convergir, o que indica que verificações adicionais contribuem menos para a capacidade de detecção.\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": [
        "Agora, compare a fidelidade do estado puro com ruído com a do estado pós-selecionado. A pós-seleção apenas das amostras nas quais não foi detectado nenhum erro aumenta o valor esperado de cada estabilizador e, consequentemente, a fidelidade estimada. Os valores pós-selecionados utilizam menos amostras do que os valores brutos; no entanto, os valores esperados são mais precisos e a variância amostral é menor. Observe também que a taxa média pós-seleção está próxima da fidelidade com ruído. É isso que se espera quando as verificações detectam quase todas as amostras com erros: a fração de amostras que são aprovadas em todas as verificações se aproxima da fração de amostras sem erros, que é a fidelidade do estado ruidoso.\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": [
        "O índice gama indica qual a proporção do canal de ruído modelado que permanece não detectada pelas verificações. Ao traçar o gráfico da pontuação gama em função do número de verificações confirmadas, fica evidente como a capacidade de detecção melhora à medida que cada verificação é adicionada. Um valor de significa `1.0` que as verificações capturam todo o ruído modelado. As curvas se inclinam em direção a `1.0` à medida que mais verificações são executadas, o que demonstra que cada verificação adicional detecta parte do erro remanescente não identificado.\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",
        "## Exemplo de hardware em grande escala\n",
        "\n",
        "O mesmo fluxo de trabalho é executado em hardware com uma capacidade útil maior e mais profunda. Esta seção reutiliza o backend do exemplo do simulador, mas cria um novo layout de 20 qubits com seus próprios pares de qubits-alvo e auxiliares, além de um gerenciador de passagens, e, em seguida, envia os circuitos para a QPU em um único trabalho. Nesse tamanho, a maioria dos disparos aciona pelo menos uma verificação; portanto, a taxa de pós-seleção é baixa, e cada circuito precisa de um grande orçamento de disparos para obter amostras suficientes para sobreviver. Portanto, o exemplo concentra seu orçamento em alguns estabilizadores selecionados; essa ainda é uma estimativa de fidelidade não sesgada, mas é mais aproximada do que a média calculada pelo simulador com base em muitos estabilizadores.\n",
        "\n",
        "Há uma diferença em relação ao exemplo do simulador: em vez de inferir um canal despolarizador uniforme a partir dos dados de calibração, esta seção **aprende** o modelo de ruído com `NoiseLearner` e constrói o modelo `qiskit-paulice` a partir do resultado com `NoiseModel.from_pauli_lindblad_maps`. Um modelo Pauli-Lindblad treinado captura a estrutura espacial do ruído nesse layout específico, em vez de supor que todas as arestas apresentem o mesmo nível de ruído; assim, a colocação do xadrez é avaliada em relação a um ruído mais semelhante ao que afeta a QPU. A aprendizagem do ruído requer amostragem da QPU e deve ser considerada em qualquer orçamento geral de amostragem da QPU.\n",
        "\n",
        "Os parâmetros a seguir definem o número de qubits, a profundidade, o número de estabilizadores e o número de disparos. A escala varia de acordo com `hw_num_shots` o inverso da taxa de pós-seleção: a uma taxa de 3%, 40.000 disparos resultam em aproximadamente 1.200 amostras pós-selecionadas por circuito. Aumente `hw_num_stabilizers` para obter uma estimativa de fidelidade mais precisa, ao custo de mais circuitos por tarefa, cada um dos quais requer o mesmo orçamento 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",
        "### Etapas 1 a 4 (reunidas em um único bloco de código)\n",
        "\n",
        "A célula a seguir executa as mesmas quatro etapas do exemplo do simulador. Ele constrói a carga útil maior e seleciona alguns estabilizadores (etapa 1); escolhe um layout, aprende o modelo de ruído nele e encontra o circuito totalmente verificado para cada estabilizador (etapa 2); envia um trabalho do Sampler que contém tanto os circuitos básicos quanto os verificados (etapa 3); e realiza a pós-seleção das contagens verificadas para comparar as estimativas de fidelidade com ruído e pós-selecionadas, por estabilizador e em média (etapa 4). Nessa escala, enumerar todo o grupo de estabilizadores, como no exemplo do simulador, é inviável; por isso, a célula realiza uma subamostragem aleatória de um subconjunto de estabilizadores para calcular uma estimativa da fidelidade.\n",
        "\n",
        "Observe que a etapa 2 tem um papel mais importante aqui do que no exemplo do simulador: o aprendizado do modelo de ruído envia seu próprio `NoiseLearner` trabalho antes do trabalho do Sampler, de modo que a célula executa **dois** trabalhos no total. Eles vêm com as etiquetas e `TUT_ASPC_LEARN` `TUT_ASPC` para que você possa encontrá-los mais tarde. Consulte [“Organizar e pesquisar por tags de vagas”](/docs/guides/add-job-tags) para saber mais sobre como marcar vagas com tags.\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": [
        "Para circuitos desse tamanho, a maioria das amostras contém pelo menos um erro detectado; portanto, a taxa de pós-seleção é baixa e a pós-seleção descarta a maioria das tomadas. As amostras que passam em todas as verificações apresentam um valor esperado muito melhor do que o circuito simples, e os valores por estabilizador se destacam claramente da linha de base ruidosa. Para refinar a estimativa de fidelidade, amostre mais estabilizadores com o mesmo orçamento de disparos por circuito. Para aumentar a taxa de pós-seleção, reduza a profundidade do circuito ou execute menos verificações; para alcançar cargas úteis maiores, dimensione o orçamento de disparos com o inverso da taxa de pós-seleção.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "next-steps",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Próximas etapas\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendações\">\n",
        "  Se você achou este trabalho interessante, talvez se interesse pelo material a seguir:\n",
        "\n",
        "  * O tutorial sobre [códigos de repetição](/docs/tutorials/repetition-codes) serve como uma introdução à correção de erros quânticos.\n",
        "  * A [documentação `qiskit-paulice`](/docs/addons/qiskit-addon-paulice) da API completa do check-finding e o [repositório GitHub](https://github.com/Qiskit/qiskit-paulice) do pacote, que contém o código-fonte.\n",
        "  * O artigo [“Detecção de erros com baixa sobrecarga usando códigos espaço-temporais”](https://arxiv.org/abs/2504.15725) apresenta a teoria por trás das verificações.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "references",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Referências\n",
        "\n",
        "* \\[1] Martiel, S., e Javadi-Abhari, A. (2025). Detecção de erros com baixa sobrecarga utilizando códigos espaço-temporais. *arXiv pré-impressão* [arXiv:2504.15725](https://arxiv.org/abs/2504.15725).\n",
        "* \\[2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., e Temme, K. (2023). Mitigação de erros em um único disparo por meio de verificações de Pauli coerentes. *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
}