Skip to main content
IBM Quantum Platform

Cancelamento probabilístico de erros com modelos lógicos de ruído

Estimativa de tempo de execução: 28 minutos em um processador Heron r3 (NOTA: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)

Rede hexagonal de Ising incorporada a um layout de qubits “heavy-hex”, com nós de Ising nos qubits “ degree-3 ” e qubits mediadores nas arestas entre eles Neste tutorial, avaliaremos os observáveis de um modelo de Ising de 22 nós em uma rede hexagonal utilizando uma QPU Heron, que possui uma topologia de qubits “heavy-hexagonal ”. Muitas demonstrações nas QPUs Heron se concentram em sistemas definidos em redes hexagonais densas para se adequarem à conectividade do sistema; no entanto, os modelos hexagonais tendem a ser mais interessantes de se estudar, pois ocorrem com mais frequência na natureza e são mais difíceis de simular por métodos clássicos devido à sua conectividade mais densa. Como a topologia de qubits da QPU não suporta diretamente a conectividade de uma rede hexagonal, precisamos decidir como incorporar de forma eficiente o modelo hexagonal à topologia de qubits. Neste exemplo, representamos cada site no modelo de Ising por um qubit situado em um vértice da rede da QPU “heavy-hex”. Utilizamos os qubits nas arestas ( qubits mediadores ) para facilitar o entrelaçamento entre os qubits de Ising nos vértices. Além disso, os qubits mediadores implementam o entrelaçamento de tal forma que sempre se espera que retornem ao estado fundamental ∣0⟩|0\rangle. Se um qubit mediador apresentar a medida ∣1⟩|1\rangle, isso indica que ocorreu um erro lógico durante a execução do circuito; a pós-seleção apenas das amostras sem erros detectados nos qubits mediadores resulta em uma distribuição menor de amostras lógicas que pode apresentar maior fidelidade do que a distribuição bruta, sujeita a ruído. Não só podemos detectar que ocorreu um erro medindo um qubit mediador, como também podemos determinar exatamente quais erros lógicos esse qubit é capaz de detectar ao longo do circuito. Ao remover de um modelo de ruído os geradores de ruído que as verificações de simetria dos qubits mediadores são capazes de detectar, obtém-se um modelo de ruído reduzido, que pode ser mitigado por meio de técnicas como o cancelamento probabilístico de erros (PEC).

Neste exemplo do caderno, combinaremos a técnica de detecção de erros descrita acima com o PEC para neutralizar o ruído quântico de forma mais eficaz do que qualquer uma das técnicas poderia fazer sozinha. Utilizaremos 49 qubits para incorporar o modelo de Ising de 22 qubits e usaremos os 27 qubits mediadores adicionais para a detecção de erros. Aplicaremos o PEC ao canal de ruído pós-selecionado para atenuar o ruído que escapa às verificações de simetria. Além de combinar a detecção de erros com o PEC, utilizaremos técnicas como a mitigação de erros de leitura TREX e verificações de erros não markovianas para combater ainda mais o efeito do ruído quântico.

O fluxo de trabalho é o seguinte:

  1. Simulação clássica dos valores esperados das observáveis para o modelo hex-Ising de 22 qubits
  2. Implementar o modelo hex-Ising de detecção de erros de 49 qubits por meio de um circuito quântico e fazer a transpilagem para o backend da QPU
  3. Especifique as camadas de entrelaçamento no circuito com samplomatic. Vamos estudar e mitigar o ruído que afeta essas camadas entrelaçadas.
  4. Adicionar verificações de erros não markovianos ao circuito
  5. Conheça o ruído de porta e o ruído de leitura que afetam o circuito
  6. Eliminar do modelo de ruído os termos detectados pelas verificações de simetria
  7. Teste o circuito de detecção de erros. Selecione posteriormente apenas as amostras que passarem em todas as verificações de simetria e de erros não markovianos e utilize a mitigação de leitura TREX para todos os cálculos de valor esperado
    • Experimente o circuito de detecção de erros
    • Faça a amostragem do circuito de detecção de erros com PEC, mas não realize a pós-seleção com base em verificações de simetria e mitigue totalmente o canal de ruído aprendido.
    • Teste o circuito de detecção de erros com o PEC. Realizar a pós-seleção com base em verificações de simetria e atenuar apenas o ruído que não pode ser detectado por essas verificações.
  8. Calcule os valores esperados e compare as estratégias. Observe-se que o QED+PEC elimina de forma mais eficaz o ruído que afeta o experimento do que qualquer um dos métodos isoladamente e produz valores esperados convergentes utilizando um número muito menor de disparos do que o exigido pelo PEC sozinho.

Requisitos

Antes de iniciar este tutorial, execute o seguinte comando para instalar todos os pacotes necessários:

%pip install networkx numpy "qiskit[visualization]" "qiskit-ibm-runtime[visualization]" samplomatic qiskit-mitigation matplotlib "qiskit-noise-learning @ git+https://github.com/Qiskit/qiskit-noise-learning.git@ieee-demo-2026"

A célula recolhida abaixo define os auxiliares de figura utilizados ao longo deste caderno.

  • # Figure helpers for this notebook (collapsed): all plotting and styling lives here.
    # The chip maps follow the style of Fig. 30 of arXiv:2607.25998.
    
    from math import comb
    
    import matplotlib.pyplot as plt
    from matplotlib import cm
    from matplotlib.colors import BoundaryNorm
    from matplotlib.patches import Circle, Rectangle, Wedge
    
    # one typography scheme for every figure in the notebook
    plt.rcParams.update(
        {
            "font.size": 13,
            "axes.titlesize": 15,
            "axes.labelsize": 13,
            "xtick.labelsize": 11,
            "ytick.labelsize": 11,
            "legend.fontsize": 11,
        }
    )
    
    
    def x_labels(layout, n_data):
        """Per-observable tick labels carrying the physical qubit indices (site i = qubit i)."""
        return [f"$X_{{{layout[i]}}}$" for i in range(n_data)]
    
    
    def plot_layout(backend, layout, n_data):
        """Chip cartoon of the embedding: data qubits green, check qubits orange."""
        from qiskit.visualization import plot_coupling_map
    
        return plot_coupling_map(
            num_qubits=backend.num_qubits,
            qubit_coordinates=None,
            coupling_map=list(backend.coupling_map.get_edges()),
            figsize=(9, 9),
            qubit_color=[
                "#4CAF50"
                if q in layout[:n_data]
                else "#FF9800"
                if q in layout[n_data:]
                else "#DDDDDD"
                for q in range(backend.num_qubits)
            ],
            qubit_size=220,
            line_width=2,
            font_size=90,
        )
    
    
    def plot_exact(obs_exact, tick_labels, title):
        plt.figure(figsize=(12, 4))
        plt.plot(obs_exact, "o-")
        plt.title(title)
        plt.xticks(np.arange(len(obs_exact)), tick_labels)
        plt.xlabel("Observable")
        plt.ylabel(r"$\langle X \rangle$")
        plt.grid()
    
    
    def draw_toy_circuit(
        generate_ed_ising, zz_coeff, x_coeff, include_checks=True
    ):
        """The boxing pipeline on a 3-plaquette-ring miniature (1 Trotter step) so the box
        structure is legible; every box carries the Twirl / InjectNoise annotations, and with
        ``include_checks`` the terminal xslow non-Markovian error check pattern is appended as in the
        production pipeline."""
        import networkx as nx
        from qiskit.circuit import ClassicalRegister
        from qiskit_mitigation.postselection import XSlowGate
        from samplomatic.transpiler import generate_boxing_pass_manager
    
        toy, _, _ = generate_ed_ising(nx.cycle_graph(3), 1, zz_coeff, x_coeff)
        toy.add_register(
            ClassicalRegister(3, "data"), ClassicalRegister(3, "check")
        )
        toy.barrier()
        toy.measure(range(3), range(3))
        toy.measure(range(3, 6), range(3, 6))
        toy_boxed = generate_boxing_pass_manager(
            enable_gates=True,
            enable_measures=True,
            inject_noise_targets="gates",
            inject_noise_strategy="individual_modification",
            inject_noise_site="after",
            twirling_strategy="active_circuit",
            measure_annotations="all",
        ).run(toy)
        if include_checks:
            toy_boxed.add_register(
                ClassicalRegister(3, "data_ps"), ClassicalRegister(3, "check_ps")
            )
            toy_boxed.barrier()
            for qb in range(6):
                toy_boxed.append(XSlowGate(), [qb])
            toy_boxed.measure(range(3), toy_boxed.cregs[2])
            toy_boxed.measure(range(3, 6), toy_boxed.cregs[3])
        return toy_boxed.draw("mpl", fold=-1, scale=0.6)
    
    
    # --- chip-level noise maps ---------------------------------------------------------
    
    BANDS = [1e-5, 2e-5, 3e-5, 4e-5, 6e-5, 1e-4, 2e-4, 3e-4, 4e-4, 6e-4, 1e-3]
    CMAP = plt.get_cmap("YlOrBr")
    NORM = BoundaryNorm(BANDS, CMAP.N, extend="both")
    
    
    def _color(rate):
        return (
            "white"
            if rate < BANDS[0]
            else CMAP(NORM(min(rate, BANDS[-1] * 0.999)))
        )
    
    
    def _layer_sparse(mit, weights):
        """Per-layer labelled terms from the saved run, box-local -> physical qubits."""
        phys = np.sort(mit["layout"])
        return [
            [
                (p, tuple(int(phys[q]) for q in qs if q >= 0), r)
                for p, qs, r in zip(
                    mit[f"label_paulis_{i}"],
                    mit[f"label_qubits_{i}"],
                    w,
                    strict=True,
                )
            ]
            for i, w in enumerate(weights)
        ]
    
    
    def _aggregate(layers):
        """w1[qubit][P] and w2[(a,b)][PaPb]: rates summed over the 3 layers."""
        w1, w2 = {}, {}
        for terms in layers:
            for p, qs, r in terms:
                if len(qs) == 1:
                    w1.setdefault(qs[0], dict.fromkeys("XYZ", 0.0))[p] += r
                else:
                    (a, pa), (b, pb) = sorted(zip(qs, p, strict=True))
                    w2.setdefault(
                        (a, b), {x + y: 0.0 for x in "XYZ" for y in "XYZ"}
                    )[pa + pb] += r
        return w1, w2
    
    
    def draw_noise_map(mit, backend, reduced=False):
        """Chip map of the learned model: X/Y/Z wheel per qubit, 3x3 two-qubit Pauli grid per
        coupler, log-banded colors, dashed outlines for unused hardware.
    
        With ``reduced=True``, keeps only the error terms the checks cannot see: detectability
        depends on circuit position, so each layer's 0/1 site scales are averaged over its uses.
        """
        from qiskit_ibm_runtime.visualization.embeddings import Embedding
    
        if reduced:
            scales = [
                mit["site_scales"][mit["site_layer"] == i].mean(axis=0)
                for i in range(3)
            ]
            weights = [mit[f"rates_{i}"] * scales[i] for i in range(3)]
            title = f"Reduced noise model ($\\gamma$ = {mit['gammas'][1]:.1f})"
        else:
            weights = [mit[f"rates_{i}"] for i in range(3)]
            title = f"Full noise model ($\\gamma$ = {mit['gammas'][0]:.0f})"
        w1, w2 = _aggregate(_layer_sparse(mit, weights))
    
        xy = np.array(
            [(c, -r) for r, c in Embedding.from_backend(backend).coordinates]
        )
        fig, ax = plt.subplots(figsize=(13, 7))
        for a, b in {tuple(sorted(e)) for e in backend.coupling_map.get_edges()}:
            if (
                (
                    a,
                    b,
                )
                in w2
            ):  # 3x3 Pauli grid laid along the bond (columns: qubit a, rows: qubit b)
                d = xy[b] - xy[a]
                u = d / np.hypot(*d)
                v = np.array([-u[1], u[0]])
                cl, cw = (np.hypot(*d) - 0.6) / 3, 0.17
                for i, Pa in enumerate("XYZ"):
                    for j, Pb in enumerate("XYZ"):
                        ax.add_patch(
                            Rectangle(
                                xy[a] + u * (0.3 + i * cl) + v * ((j - 1.5) * cw),
                                cl,
                                cw,
                                angle=np.degrees(np.arctan2(u[1], u[0])),
                                facecolor=_color(w2[a, b][Pa + Pb]),
                                edgecolor="black",
                                lw=0.4,
                                zorder=2,
                            )
                        )
            else:
                ax.plot(
                    *zip(xy[a], xy[b], strict=True),
                    ls="--",
                    lw=0.7,
                    color="black",
                    alpha=0.5,
                    zorder=1,
                )
        for q, (xq, yq) in enumerate(xy):
            if q in w1:  # three-sector wheel: X top, Y lower left, Z lower right
                for P, t0 in (("X", 30), ("Y", 150), ("Z", 270)):
                    ax.add_patch(
                        Wedge(
                            (xq, yq),
                            0.3,
                            t0,
                            t0 + 120,
                            facecolor=_color(w1[q][P]),
                            edgecolor="black",
                            lw=0.6,
                            zorder=3,
                        )
                    )
                ax.annotate(
                    str(q),
                    (xq + 0.39, yq - 0.39),
                    fontsize=9,
                    color="gray",
                    ha="left",
                    va="top",
                    zorder=4,
                )  # southeast, clear of the bond grids
            else:
                ax.add_patch(
                    Circle(
                        (xq, yq),
                        0.24,
                        facecolor="none",
                        edgecolor="black",
                        ls="--",
                        lw=0.7,
                        alpha=0.5,
                        zorder=3,
                    )
                )
        ux = xy[sorted(w1)]
        ax.set_xlim(ux[:, 0].min() - 2.2, ux[:, 0].max() + 2.2)
        ax.set_ylim(ux[:, 1].min() - 1.6, ux[:, 1].max() + 1.6)
        ax.set_aspect("equal")
        ax.axis("off")
        ax.set_title(title, fontsize=15)
        cb = fig.colorbar(
            cm.ScalarMappable(norm=NORM, cmap=CMAP),
            ax=ax,
            fraction=0.035,
            pad=0.02,
            extend="both",
            ticks=BANDS,
        )
        cb.set_label("coefficient (log bands; white < 1e-05)", fontsize=11)
        cb.ax.set_yticklabels(
            [f"{b:.0e}".replace("e-0", "e-") for b in BANDS], fontsize=10
        )
        _noise_map_legends(fig, ax)
    
    
    def _noise_map_legends(fig, ax):
        """Weight-1 wheel and weight-2 grid keys, in a reserved band left of the lattice."""
        fig.subplots_adjust(left=0.17)
        axl = ax.inset_axes([-0.185, 0.70, 0.13, 0.24])
        for P, t0 in (("X", 30), ("Y", 150), ("Z", 270)):
            axl.add_patch(
                Wedge(
                    (0.5, 0.45),
                    0.38,
                    t0,
                    t0 + 120,
                    facecolor="white",
                    edgecolor="black",
                    lw=0.8,
                )
            )
            axl.annotate(
                P,
                (
                    0.5 + 0.2 * np.cos(np.radians(t0 + 60)),
                    0.45 + 0.2 * np.sin(np.radians(t0 + 60)),
                ),
                ha="center",
                va="center",
                fontsize=9,
            )
        axl.set_title("weight-1", fontsize=10)
        axl.set_xlim(0, 1)
        axl.set_ylim(0, 1)
        axl.set_aspect("equal")
        axl.axis("off")
        axm = ax.inset_axes([-0.185, 0.32, 0.14, 0.30])
        for i, Pa in enumerate("XYZ"):
            for j, Pb in enumerate("XYZ"):
                axm.add_patch(
                    Rectangle(
                        (i / 3, 1 - (j + 1) / 3),
                        1 / 3,
                        1 / 3,
                        facecolor="white",
                        edgecolor="black",
                        lw=0.6,
                    )
                )
                axm.annotate(
                    Pa + Pb,
                    ((i + 0.5) / 3, 1 - (j + 0.5) / 3),
                    ha="center",
                    va="center",
                    fontsize=7.5,
                )
            axm.annotate(Pa, ((i + 0.5) / 3, 1.08), ha="center", fontsize=8.5)
            axm.annotate(
                "XYZ"[i],
                (-0.13, 1 - (i + 0.5) / 3),
                ha="center",
                va="center",
                fontsize=8.5,
            )
        axm.annotate("qubit a", (0.5, 1.27), ha="center", fontsize=9)
        axm.annotate(
            "qubit b", (-0.33, 0.5), rotation=90, va="center", fontsize=9
        )
        axm.set_xlim(-0.35, 1.05)
        axm.set_ylim(-0.05, 1.35)
        axm.set_aspect("equal")
        axm.axis("off")
    
    
    # --- run diagnostics ---------------------------------------------------------------
    
    
    def plot_trex(trex_rescale, tick_labels):
        _fig, axt = plt.subplots(figsize=(12, 3))
        axt.stem(np.arange(len(trex_rescale)), (trex_rescale - 1) * 100)
        axt.set_xticks(np.arange(len(trex_rescale)), tick_labels)
        axt.set_xlabel("Observable")
        axt.set_ylabel("Readout correction (%)")
        axt.set_title("TREX rescale factors")
        plt.tight_layout()
    
    
    def plot_postselection(mit):
        """Accepted shots per PEC randomization against a single binomial at the mean
        acceptance rate: agreement means acceptance is independent of the sampled circuit
        instance, the condition under which pooling accepted shots across randomizations
        is a consistent estimator."""
        _fig, ax = plt.subplots(figsize=(7.5, 3.8))
        counts = mit["acc_counts_post"]
        K, p = 64, counts.mean() / 64
        ks = np.arange(K + 1)
        pmf = np.array([comb(K, k) * p**k * (1 - p) ** (K - k) for k in ks])
        ax.hist(
            counts,
            bins=np.arange(-0.5, K + 1.5),
            density=True,
            alpha=0.6,
            color="#da1e28",
            label="measured",
        )
        ax.plot(ks, pmf, "k-", lw=1.5, label=f"Binomial(64, {p:.3f})")
        ax.set_xlim(-0.5, max(int(counts.max()) + 3, 20))
        ax.set_xlabel("Accepted shots per randomization")
        ax.set_ylabel("Probability")
        ax.legend()
        ax.grid(alpha=0.4)
        plt.tight_layout()
    
    
    def plot_convergence(mit, obs_exact, n_data):
        """Running site-averaged estimate vs. randomizations for both PEC arms (the S5-consistent
        signed-ratio estimator, evaluated on growing prefixes of the sweep)."""
    
        def running(prefix):
            bits = np.squeeze(
                np.unpackbits(mit[f"data_{prefix}"], axis=-1)[..., :n_data]
                ^ mit[f"flips_{prefix}"]
            )
            mask = np.squeeze(mit[f"mask_{prefix}"])
            signs = 1 - 2 * (np.squeeze(mit[f"signs_{prefix}"]).sum(axis=-1) % 2)
            qv = (1 - 2 * bits.astype(int)) * mit["trex_rescale"]
            u = (
                (signs[:, None, None] * mask[..., None] * qv)
                .sum(axis=1)
                .mean(axis=1)
            )
            v = signs * mask.sum(axis=1)
            Rs = np.arange(500, len(u) + 1, 500)
            est, err = [], []
            for R in Rs:
                e = u[:R].sum() / v[:R].sum()
                est.append(e)
                err.append(
                    np.sqrt(((u[:R] - e * v[:R]) ** 2).sum()) / abs(v[:R].sum())
                )
            return Rs, np.array(est), np.array(err)
    
        _fig, ax = plt.subplots(figsize=(12, 5))
        ideal_avg = float(np.mean(obs_exact))
        ax.axhline(ideal_avg, color="black", label="ideal")
        ax.fill_between(
            [-400, 24000],
            ideal_avg - 0.025,
            ideal_avg + 0.025,
            color="grey",
            alpha=0.22,
            label=r"$\pm 0.025$",
        )
        for prefix, label, color in (
            ("van", "vanilla PEC", "#8a3ffc"),
            ("post", "PEC + error detection", "#da1e28"),
        ):
            Rs, est, err = running(prefix)
            ax.errorbar(
                Rs,
                est,
                yerr=err,
                marker="o",
                linestyle="",
                markerfacecolor="none",
                color=color,
                alpha=0.85,
                capsize=3,
                label=label,
            )
        ax.set_xlim(-400, 24000)
        ax.set_ylim(ideal_avg - 0.08, ideal_avg + 0.08)
        ax.set_xlabel("# randomizations")
        ax.set_ylabel(r"Site-averaged $\langle X \rangle$")
        ax.legend(ncols=2)
    
    
    def plot_final(
        obs_exact, baseline, ed, pec, post, gammas, tick_labels, title
    ):
        """Per-site <X> for every method, with an rms-deviation inset.
        baseline/ed/pec/post are (values, errors) pairs; gammas is (gamma, gamma_post)."""
        x = np.arange(len(obs_exact))
        _fig, ax = plt.subplots(figsize=(13, 5))
        h_ideal = ax.errorbar(
            x, obs_exact, fmt="-", capsize=4, label="ideal", color="black"
        )
        h_base = ax.errorbar(
            x,
            baseline[0],
            yerr=baseline[1],
            fmt=".--",
            capsize=4,
            label="baseline",
            color="#0f62fe",
        )
        h_ed = ax.errorbar(
            x,
            ed[0],
            yerr=ed[1],
            fmt="^",
            capsize=4,
            label="error detection",
            color="#009d9a",
            alpha=0.8,
        )
        h_pec = ax.errorbar(
            x,
            pec[0],
            yerr=pec[1],
            fmt="x",
            capsize=4,
            label=f"PEC ($\\gamma$={gammas[0]:.0f})",
            color="#8a3ffc",
            alpha=0.7,
        )
        h_post = ax.errorbar(
            x,
            post[0],
            yerr=post[1],
            fmt="d",
            capsize=4,
            label=f"PEC + error detection ($\\gamma$={gammas[1]:.0f})",
            color="#da1e28",
        )
        ax.set_xticks(x, tick_labels)
        ax.set_xlabel("Observable")
        ax.set_ylabel("Expectation value")
        # Set the y-axis range using the ideal, baseline, error detection, and PEC + error
        # detection curves, including error bars. Exclude PEC-only values when setting the
        # range so large fluctuations do not make the other curves difficult to distinguish.
        # PEC-only values may fall outside the visible range. Leave room above for the inset.
        series = [np.asarray(obs_exact)] + [
            np.asarray(v) + s * np.asarray(e)
            for v, e in (baseline, ed, post)
            for s in (-1, 1)
        ]
        lo, hi = min(a.min() for a in series), max(a.max() for a in series)
        span = max(hi - lo, 0.1)
        ax.set_ylim(lo - 0.1 * span, hi + 1.05 * span)
        # legend ordered to match the curves' vertical positions in the chart
        ax.legend(
            handles=[h_ideal, h_pec, h_post, h_ed, h_base],
            ncols=5,
            loc="lower center",
            bbox_to_anchor=(0.5, 1.02),
            frameon=False,
        )
        ax.set_title(title, pad=44)
    
        # inset: rows bottom-to-top so it reads top-to-bottom: PEC, PEC+ED, ED, baseline
        axi = ax.inset_axes([0.36, 0.68, 0.28, 0.26])
        methods = [
            ("baseline", baseline[0], "#0f62fe"),
            ("QED", ed[0], "#009d9a"),
            ("PEC+QED", post[0], "#da1e28"),
            ("PEC", pec[0], "#8a3ffc"),
        ]
        for k, (_nm, vals, color) in enumerate(methods):
            axi.barh(
                k,
                np.sqrt(np.mean((vals - np.array(obs_exact)) ** 2)),
                color=color,
                alpha=0.9,
            )
        axi.set_yticks(range(4), [m[0] for m in methods], fontsize=10)
        axi.set_title("RMS deviation from ideal", fontsize=11)
        axi.tick_params(labelsize=10)
        axi.patch.set_alpha(1.0)
        axi.set_zorder(5)

Simular de forma clássica o processo “ ⟨X⟩i\langle X \rangle_i ” para cada nó no modelo hex-Ising de 22 qubits

Primeiro, usamos a classe Statevector Qiskit para simular os valores-alvo exatos para nosso experimento. Embora o problema de 22 qubits que estamos resolvendo seja tratável classicamente, o simulador de vetor de estado do Qiskit não consegue lidar com demonstrações muito maiores do que esta; e, para ampliar a escala para além de cerca de 50 qubits, precisaríamos usar técnicas de simulação inexatas, como a propagação de Pauli. Este experimento com 49 qubits nos permite investigar a combinação da detecção de erros com a mitigação de erros em escalas maiores, mantendo o acesso aos valores esperados ideais.

import warnings

import networkx as nx
import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import Pauli, Statevector

# Silence two harmless upstream warnings (a Samplomatic default-change notice and a
# numpy datetime timezone notice from the noise-learning circuit generator)
warnings.filterwarnings(
    "ignore", message="The default of the 'inject_noise_site'"
)
warnings.filterwarnings(
    "ignore", message="no explicit representation of timezones"
)

# Hexagonal lattice
data_graph = nx.convert_node_labels_to_integers(
    nx.hexagonal_lattice_graph(2, 3)
)

# Number of Trotter steps and rotation angles
depth = 4
zz_coeff = -np.pi / 4
x_coeff = 3 * np.pi / 8
n_data = data_graph.order()
n_checks = data_graph.size()
n_qubits = n_data + n_checks

qc_data = QuantumCircuit(n_data)
qc_data.h(range(n_data))
for _ in range(depth):
    for edge in data_graph.edges:
        qc_data.rzz(zz_coeff, *edge)
    qc_data.rx(x_coeff, range(n_data))
psi_exact = Statevector(qc_data)

# X on site i = qubit i
observables = ["I" * (n_data - 1 - i) + "X" + "I" * i for i in range(n_data)]
obs_exact = [psi_exact.expectation_value(Pauli(o)).real for o in observables]

plot_exact(
    obs_exact,
    x_labels(range(n_data), n_data),
    f"Statevector simulation, {n_data} qubit hex-Ising, {depth} Trotter steps",
)

Output:

Output of the previous code cell

Implementar o modelo hex-Ising de detecção de erros de 49 qubits por meio de um circuito quântico e fazer a transpilagem para o backend da QPU

Este circuito simula 4 passos de Trotter de um hamiltoniano de Ising de campo transversal de 22 qubits em uma rede hexagonal. Os 22 qubits de dados estão incorporados em um subgrafo “heavy-hex” de 49 qubits da QPU Heron r3ibm_boston. Os 27 qubits auxiliares são utilizados para dois fins: mediar o entrelaçamento entre os qubits de Ising e detectar erros lógicos durante a execução do circuito. As camadas de entrelaçamento do circuito são implementadas de forma que os qubits mediadores retornem ao estado fundamental ∣0⟩|0\rangle antes das medições finais. Um ou mais qubits mediadores que apresentem o valor “ ∣1⟩|1\rangle ” indicam que a amostra foi corrompida por um erro lógico. Descartar essas amostras pode melhorar a precisão da distribuição amostrada.

No gráfico abaixo, os qubits verdes representam os 22 qubits de Ising, e os qubits laranja representam os 27 qubits mediadores.

from qiskit.circuit import ClassicalRegister
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

backend_name = "ibm_boston"


def generate_ed_ising(graph, depth, zz_coeff, x_coeff):
    """Build the mediated error-detecting Ising circuit for data-qubit `graph`; returns (circuit, CZ layers, hardware graph)."""
    hw_graph = nx.Graph()
    for i, (a, b) in enumerate(graph.edges()):
        hw_graph.add_edges_from(
            [(a, i + graph.order()), (b, i + graph.order())]
        )
    coloring = nx.coloring.greedy_color(
        nx.line_graph(hw_graph), strategy="DSATUR"
    )
    layers_coupling = [
        [e for e, c in coloring.items() if c == i]
        for i in set(coloring.values())
    ]
    circuit = QuantumCircuit(hw_graph.order())
    circuit.h(range(hw_graph.order()))
    for _ in range(depth):
        for angle, qubits in (
            (zz_coeff, range(graph.order(), hw_graph.order())),
            (x_coeff, range(graph.order())),
        ):
            circuit.barrier()
            for layer in layers_coupling:
                for edge in layer:
                    circuit.cz(*edge)
            circuit.barrier()
            circuit.rx(angle, qubits)
    circuit.barrier()
    circuit.h(range(graph.order(), hw_graph.order()))
    return circuit, layers_coupling, hw_graph


service = QiskitRuntimeService()
backend = service.backend(backend_name)

circuit, layers_coupling, hw_graph = generate_ed_ising(
    data_graph, depth, zz_coeff, x_coeff
)

circ_meas = circuit.copy()
data_reg, check_reg = (
    ClassicalRegister(n_data, "data"),
    ClassicalRegister(n_checks, "check"),
)
circ_meas.add_register(data_reg, check_reg)
circ_meas.barrier()
circ_meas.measure(range(n_data), data_reg)
circ_meas.measure(range(n_data, n_qubits), check_reg)

# Physical qubits hosting the 22 Ising sites on ibm_boston, one heavy-hex row per lattice chain;
# the mediator for each edge is the physical qubit sitting between its two data qubits
data_layout = [
    *[95, 93, 91, 89, 87],
    *[115, 113, 111, 109, 107, 105],
    *[135, 133, 131, 129, 127, 125],
    *[155, 153, 151, 149, 147],
]
coupling_graph = nx.Graph(list(backend.coupling_map.get_edges()))
layout = data_layout + [
    next(
        iter(
            nx.common_neighbors(
                coupling_graph, data_layout[a], data_layout[b]
            )
        )
    )
    for a, b in data_graph.edges()
]

circ_trans = generate_preset_pass_manager(
    backend=backend, optimization_level=1, initial_layout=layout
).run(circ_meas)

plot_layout(backend, layout, n_data)

Output:

Output of the previous code cell

Especifique as camadas de entrelaçamento no circuito com samplomatic.

O grupo generate_boxing_pass_manager “pass” do Samplomatic agrupa as camadas entrelaçadas do circuito em caixas anotadas para a rotação de Pauli e a injeção de ruído PEC. O circuito-modelo resultante e o samplex (uma distribuição paramétrica sobre o circuito-modelo que especifica seus giros e a injeção de ruído) são utilizados para definir e executar todos os experimentos de aprendizado com ruído e amostragem na QPU. As caixas de medição também contêm uma ChangeBasis anotação; assim, as rotações que medem os qubits de dados na base X são fornecidas ao samplex como entrada no momento da amostragem, em vez de serem incorporadas ao circuito.

A seguir, a mesma técnica de “boxing” é aplicada a uma versão reduzida do circuito de Ising com detecção de erros, para que possamos visualizar a estrutura de um circuito com “boxing”.

from samplomatic.builders import build
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions

boxed = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
    inject_noise_targets="gates",
    inject_noise_strategy="individual_modification",
    inject_noise_site="after",
    twirling_strategy="active_circuit",
    measure_annotations="all",
).run(circ_trans)
template, samplex = build(boxed)
unique_instructions = find_unique_box_instructions(
    boxed, normalize_annotations=None, undress_boxes=True
)

# Measure X on the data qubits and Z on the mediators
basis_key = next(
    s.name
    for s in samplex.inputs().get_specs()
    if s.name.startswith("basis_changes.")
)
meas_basis = np.array(
    [2 if q in set(layout[:n_data]) else 1 for q in sorted(layout)],
    dtype=np.uint8,
)

draw_toy_circuit(generate_ed_ising, zz_coeff, x_coeff, include_checks=False)

Output:

Output of the previous code cell

Adicionar verificações de erros não markovianos ao circuito

Em seguida, adicionamos verificações de erros não markovianos ao circuito, a fim de nos proteger contra ruídos que não estão modelados em nosso protocolo de aprendizado. Essas verificações funcionam por meio da implementação de uma inversão de bits com pulso longo e da garantia de que o qubit tenha passado corretamente de um estado clássico para outro. Se ambos os qubits de uma aresta não passarem na verificação, a amostra é descartada. As verificações de erro não markovianas podem ser utilizadas no início ou no final do circuito (ou em ambos); neste caso, nós as utilizamos apenas no final. Também colocamos qubits “espectadores” não utilizados adjacentes ao circuito, proporcionando maior cobertura contra o ruído que esses qubits foram projetados para detectar.

Lembre-se de que, assim como nas verificações de simetria, isso também é uma forma de pós-seleção; portanto, precisamos garantir que estejamos levando em conta a redução na taxa de pós-seleção ao combinar essas técnicas. Concretamente, se P(non-Markovian error)=αP(\text{non-Markovian error}) = \alpha e P(symmetry error)=βP(\text{symmetry error}) = \beta, então são necessárias aproximadamente 1(1−α)(1−β)\frac{1}{(1-\alpha)(1-\beta)} amostras com ruído para recuperar uma amostra lógica.

from qiskit.transpiler import PassManager
from qiskit_mitigation.postselection import PostSelector
from qiskit_mitigation.postselection.passes import (
    AddPostCircuitNonMarkovianErrorChecks,
    AddSpectatorPostCircuitNonMarkovianErrorChecks,
)

add_checks = PassManager(
    [
        AddPostCircuitNonMarkovianErrorChecks(x_pulse_type="xslow"),
        AddSpectatorPostCircuitNonMarkovianErrorChecks(
            backend.coupling_map, x_pulse_type="xslow"
        ),
    ]
)
template_checked = add_checks.run(template)
selector = PostSelector.from_circuit(template_checked, backend.coupling_map)

draw_toy_circuit(generate_ed_ising, zz_coeff, x_coeff, include_checks=True)

Output:

Output of the previous code cell

Conheça o ruído de porta e de leitura do circuito Ising verificado de 49 qubits

Para mitigar o ruído, precisamos modelar como ele está afetando nossas portas de entrelaçamento. Aqui, aprendemos um modelo de ruído de Pauli-Lindblad para cada uma das três camadas de entrelaçamento exclusivas do circuito, utilizando o qiskit-noise-learning. Posteriormente, eliminaremos desse modelo de ruído os geradores de erro que são detectáveis pelas verificações de simetria e mitigaremos apenas o canal de ruído remanescente com o PEC. O mapa abaixo mostra as três camadas aprendidas na QPU; cada qubit é uma roda com suas taxas de X/Y/Z weight-1, e cada acoplador é uma grade 3×3 das taxas de Pauli weight-2 para aquele par. O valor de γ\gamma de 1010 implica uma sobrecarga de amostragem de γ2=102=100\gamma^2=10^2=100.

As correções de leitura (TREX) provêm diretamente dos caminhos SPAM do ajuste com aprendizado de ruído. O gráfico de hastes abaixo do mapa de ruído do QPU mostra os fatores de reescalonamento do TREX por observável.

from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauli
from qiskit_ibm_runtime import Executor, Session
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from qiskit_mitigation.trex import TREX
from qiskit_noise_learning.analysis import (
    ComputeObservables,
    CurveFitObservables,
    FlipPostSelect,
    LeastSquaresSolve,
)
from qiskit_noise_learning.circuit_generator import ExecutorCircuitGenerator
from qiskit_noise_learning.experiment_builder import (
    BindFragmentDepths,
    CompleteSequences,
    EvenDepthVanillaPaths,
    Experiment,
    GenerateInstructionSequences,
    IdentifyRelations,
    MergeInstructionSequences,
    SPAMPaths,
    VanillaInstructionSequences,
)
from qiskit_noise_learning.gate_sets import QiskitGateSet
from qiskit_noise_learning.models import PauliLindbladModel
from qiskit_noise_learning.models.utils import split_pauli_lindblad_model
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation

gate_set = QiskitGateSet(
    target=backend.target,
    qubit_subset=sorted(
        {
            boxed.find_bit(q).index
            for i in unique_instructions
            for q in i.qubits
        }
    ),
)
ref_to_qubits = {}
for inst in unique_instructions:
    if ann := get_annotation(inst.operation, InjectNoise):
        gate_set.add_box_as_gate(inst, name=ann.ref)
        ref_to_qubits[ann.ref] = sorted(
            boxed.find_bit(q).index for q in inst.qubits
        )

fidelity_model = PauliLindbladModel.k_local(
    gate_set, gate_k={**{r: 2 for r in ref_to_qubits}, "M": 1, "P": 1}
)
experiment = (
    EvenDepthVanillaPaths()
    + VanillaInstructionSequences()
    + IdentifyRelations()
    + SPAMPaths()
    + GenerateInstructionSequences()
    + MergeInstructionSequences()
    + CompleteSequences()
    + BindFragmentDepths([2, 4, 8, 12])
).run(Experiment(fidelity_model=fidelity_model, shots=384, randomizations=64))

circuit_generator = ExecutorCircuitGenerator(
    gate_set, pass_manager=add_checks
)
program_learn, data_mapper = circuit_generator.generate(experiment)

session = Session(backend)
fit = circuit_generator.collect(
    Executor(session).run(program_learn).result(), data_mapper
)
fit = (
    FlipPostSelect()
    + ComputeObservables()
    + CurveFitObservables()
    + LeastSquaresSolve()
).run(fit)
print(
    "learning shots removed by checks:",
    f"{float(fit.raw_data.datatree['0']['data_mask'].mean()):.4f}",
)

# TREX factors from the fit's SPAM paths: the fit only identifies the product of
# state-prep and measurement error, so hand TREX the composition of the two maps
spam = fidelity_model.to_pauli_lindblad_maps(
    fit.model_data, include_spam=True
)
spam_map = spam["P"].compose(spam["M"])
z_terms = [
    QubitSparsePauli(("Z", [layout[i]]), num_qubits=backend.num_qubits)
    for i in range(n_data)
]
trex_rescale = np.array(
    [TREX.calculate_trex_factor(spam_map, z) for z in z_terms]
)

# learned maps come back in backend qubit indexing; samplex wants box-local order
plm = split_pauli_lindblad_model(fit.model).model
noise_maps = {}
for ref, m in plm.to_pauli_lindblad_maps(fit.model_data).items():
    box = ref_to_qubits[ref]
    noise_maps[ref] = PauliLindbladMap.from_sparse_list(
        [
            (p, tuple(box.index(q) for q in qs), r)
            for p, qs, r in m.to_sparse_list()
        ],
        num_qubits=len(box),
    )
# Full-PEC cost: each learned layer acts twice per Trotter step (compute + uncompute)
gamma = float(
    np.exp(2 * depth * sum(2 * sum(m.rates) for m in noise_maps.values()))
)

# Collect the learned model in the form the figure helpers expect
mit = dict(
    **{
        f"rates_{i}": np.asarray(m.rates)
        for i, m in enumerate(noise_maps.values())
    },
    **{
        f"label_paulis_{i}": np.array([p for p, _, _ in m.to_sparse_list()])
        for i, m in enumerate(noise_maps.values())
    },
    **{
        f"label_qubits_{i}": np.array(
            [
                list(qs) + [-1] * (2 - len(qs))
                for _, qs, _ in m.to_sparse_list()
            ]
        )
        for i, m in enumerate(noise_maps.values())
    },
    layout=np.array(layout),
    trex_rescale=trex_rescale,
    gammas=np.array([gamma, np.nan]),
)
draw_noise_map(mit, backend)
plot_trex(mit["trex_rescale"], x_labels(layout, n_data))

Output:

learning shots removed by checks: 0.2393
Output of the previous code cell Output of the previous code cell

Eliminar do modelo de ruído os termos detectáveis pelas verificações de simetria

Cada verificação de simetria é capaz de detectar um subconjunto de geradores de erros no modelo de ruído, o que significa que você não precisa mitigar esses erros. Aqui, criamos uma máscara sobre os geradores de ruído detectáveis usando a função create_postselected_noise_mask de qiskit_mitigation. Essa máscara será usada para refinar o modelo de ruído dos termos detectáveis antes de realizar o PEC. A realização do PEC com base no modelo de ruído podado pode gerar valores esperados convergentes por uma fração do custo de amostragem necessário para realizar o PEC no modelo de ruído completo, conforme ilustrado nos mapas do modelo de ruído acima.

O mapa abaixo foi elaborado com a mesma escala de cores do modelo treinado acima, mantendo apenas os geradores de erros que as verificações não conseguem detectar. Percebemos que um grande número de geradores de erros foi removido do modelo, e a sobrecarga de amostragem diminuiu significativamente. A sobrecarga de amostragem para executar o PEC no modelo de ruído podado é de γ2=2.52≈6.3\gamma^2=2.5^2\approx6.3, o que representa uma redução de aproximadamente 16x em relação à sobrecarga necessária para mitigar o canal de ruído completo.

from qiskit.quantum_info import Pauli
from qiskit_mitigation.noise import create_postselected_noise_mask

SHOTS_PER_RAND = 64  # shots per PEC randomization
N_RAND = 23_054  # PEC randomizations per experiment
MAX_PAIR_RATE = 0.02  # Max value of any coupler's summed two-qubit error rate

# The detectors are virtual Pauli-Z's representing the measurements on the mediator qubits
# We can find the set of detectable noise generators in the model for each detector by conjugating
# it backward through the circuit and calculating what generators it anti-commutes with.
detectors = [
    Pauli("I" * (boxed.num_qubits - 1 - q) + "Z" + "I" * q)
    for q in layout[n_data:]
]
# Detectable generators are handled by postselection; prune them from the model for PEC
local_scales, gamma2_post = create_postselected_noise_mask(
    boxed, noise_maps, detectors
)
gamma_post = gamma2_post**0.5
print(f"gamma PEC {gamma:.2f} | gamma QED+PEC {gamma_post:.2f}")

# Kill the job if any coupler's summed rate exceeds MAX_PAIR_RATE
pair_lam = {}
for m in noise_maps.values():
    for _, qs, r in m.to_sparse_list():
        if len(qs) == 2:
            pair_lam[tuple(sorted(qs))] = (
                pair_lam.get(tuple(sorted(qs)), 0.0) + r
            )
assert (
    max(pair_lam.values()) < MAX_PAIR_RATE
), f"kill: degraded coupler {max(pair_lam, key=pair_lam.get)}"

# Detectability depends on circuit position, so record each site's 0/1 scales by layer
site_ref = {
    a.modifier_ref: a.ref
    for inst in boxed.data
    if inst.operation.name == "box"
    and (a := get_annotation(inst.operation, InjectNoise))
    and a.ref
}
sites = sorted(local_scales, key=lambda s: int(s[1:]))
mit["site_scales"] = np.stack([local_scales[s] for s in sites])
mit["site_layer"] = np.array(
    [list(noise_maps).index(site_ref[s]) for s in sites]
)
mit["gammas"] = np.array([gamma, gamma_post])
draw_noise_map(mit, backend, reduced=True)

Output:

gamma PEC 10.08 | gamma QED+PEC 2.52
Output of the previous code cell

Experimente o circuito de detecção de erros

Aqui, analisamos o circuito de Ising de 49 qubits com detecção de erros. Habilitamos o “Pauli twirling” e as verificações de erros não markovianas, mas não realizamos amostragem PEC. Utilizaremos essas amostras para calcular os valores esperados de referência, bem como os valores esperados exclusivos para a detecção de erros.

# Baseline = the ED+PEC template itself at noise scale 0, i.e. twirling only
program_tw = QuantumProgram(shots=100)
program_tw.append_samplex_item(
    template_checked,
    samplex=samplex,
    shape=(1000, 1),
    samplex_arguments={
        "pauli_lindblad_maps": dict(noise_maps),
        basis_key: meas_basis,
        **{f"noise_scales.{k}": 0.0 for k in local_scales},
    },
)
job_tw = Executor(session).run(program_tw)

Teste o circuito de detecção de erros com o PEC

Agora, fazemos uma nova amostragem e incluímos randomizações PEC para atenuar o ruído que as verificações de simetria não conseguem detectar. Essas amostras serão utilizadas para calcular os valores esperados, por meio da detecção de erros em combinação com o PEC. A seguir, representamos graficamente o número de fotos aceitas por randomização PEC em comparação com uma distribuição binomial na taxa média de aceitação. Vemos que a aceitação não está correlacionada com a instância do circuito amostrada, pois o QED+PEC injeta apenas Paulis indetectáveis, que comutam com as medições de cada qubit mediador.

def launch(session, template, samplex, samplex_args, n_rand, shots_per_rand):
    """Sample `n_rand` randomizations of `template` on `session` in <=100k-randomization jobs; returns the jobs."""
    jobs = []
    for start in range(0, n_rand, 100_000):
        program = QuantumProgram(shots=shots_per_rand)
        program.append_samplex_item(
            template,
            samplex=samplex,
            samplex_arguments=samplex_args,
            shape=(min(100_000, n_rand - start), 1),
        )
        jobs.append(Executor(session).run(program))
    return jobs


# Reduced PEC
args_post = {
    "pauli_lindblad_maps": dict(noise_maps),
    basis_key: meas_basis,
    **{f"noise_scales.{k}": -1.0 for k in local_scales},
    **{f"local_scales.{k}": v for k, v in local_scales.items()},
}
# PEC on the full noise model
args_van = {
    "pauli_lindblad_maps": dict(noise_maps),
    basis_key: meas_basis,
    **{f"noise_scales.{k}": -1.0 for k in local_scales},
}

# Run sampling jobs
jobs = launch(
    session, template_checked, samplex, args_post, N_RAND, SHOTS_PER_RAND
)
jobs_van = launch(
    session, template_checked, samplex, args_van, N_RAND, SHOTS_PER_RAND
)
outs = [j.result()[0] for j in jobs]
outs_van = [j.result()[0] for j in jobs_van]
(out_tw,) = job_tw.result()
session.close()

Colete as amostras e verifique se as randomizações do PEC comutam com as medições nos qubits mediadores e não afetam as estatísticas da pós-seleção.

def bitflip_mask(out, selector):
    """Per-shot True/False for job result `out`: shot passes `selector`'s non-Markovian error checks."""
    regs = {
        k: np.asarray(out[k])
        for k in out
        if not k.startswith(("measurement_flips", "pauli_signs"))
    }
    return selector.compute_mask(regs, "edge", mode="post")


def symmetry_mask(out):
    """Per-shot True/False for job result `out`: every twirl-corrected symmetry check reads 0."""
    return ~(
        np.asarray(out["check"]) ^ np.asarray(out["measurement_flips.check"])
    ).any(axis=-1)


def keep_mask(out, selector):
    """Per-shot True/False for job result `out`: shot passes both check types."""
    return symmetry_mask(out) & bitflip_mask(out, selector)


def fracs(out_list, selector):
    """Fractions of shots across the job results in `out_list` passing [no, non-Markovian error, symmetry, both] checks."""
    bf = np.concatenate([bitflip_mask(o, selector) for o in out_list])
    sy = np.concatenate([symmetry_mask(o) for o in out_list])
    return [1.0, bf.mean(), sy.mean(), (bf & sy).mean()]


mask_post = np.concatenate([keep_mask(o, selector) for o in outs])

# Collect the sampled data alongside the learned model, in the form the figure helpers expect
mit.update(
    ps_fracs=np.array(
        [
            fracs([out_tw], selector),
            fracs(outs_van, selector),
            fracs(outs, selector),
        ]
    ),
    acc_counts_post=np.squeeze(mask_post).sum(axis=-1),
    data_tw=np.asarray(out_tw["data"]),
    flips_tw=np.asarray(out_tw["measurement_flips.data"]),
    mask_tw=keep_mask(out_tw, selector),
    data_post=np.packbits(
        np.concatenate([np.asarray(o["data"]) for o in outs]), axis=-1
    ),
    flips_post=np.concatenate(
        [np.asarray(o["measurement_flips.data"]) for o in outs]
    ),
    signs_post=np.concatenate([np.asarray(o["pauli_signs"]) for o in outs]),
    mask_post=mask_post,
    data_van=np.packbits(
        np.concatenate([np.asarray(o["data"]) for o in outs_van]), axis=-1
    ),
    flips_van=np.concatenate(
        [np.asarray(o["measurement_flips.data"]) for o in outs_van]
    ),
    signs_van=np.concatenate(
        [np.asarray(o["pauli_signs"]) for o in outs_van]
    ),
    mask_van=np.concatenate([bitflip_mask(o, selector) for o in outs_van]),
)


# Plot the PEC bias check
plot_postselection(mit)

Output:

Output of the previous code cell

Calcular valores esperados e comparar estratégias

Por fim, calculamos todos os valores esperados com a função auxiliar executor_expectation_values de qiskit-mitigation, que aplica para nós as inversões de medição, as máscaras de pós-seleção, os fatores de reescalonamento do TREX e os sinais de quase-probabilidade.

Gráfico superior: Observamos que ambas as variantes do PEC convergem, mas o QED+PEC atinge a banda de ±0.025\pm 0.025 e com menos randomizações e barras de erro mais estreitas. O viés residual no cálculo QED+PEC está abaixo de 0.01 e pode ser atribuído a uma combinação de divergências no modelo de ruído, desvio dos qubits ao longo do tempo e efeitos decorrentes de fontes de ruído não modeladas.

Gráfico inferior : A estimativa acumulada do valor médio por local do parâmetro “ ⟨X⟩\langle X \rangle ” à medida que as randomizações se acumulam, utilizando o mesmo número de medições por randomização para ambas as variantes do PEC. A detecção de erros com PEC+ se estabiliza na faixa após algumas milhares de randomizações; o método apenas com PEC, que arca com toda a sobrecarga de amostragem, requer muito mais randomizações para convergir.

from qiskit.quantum_info import SparsePauliOp
from qiskit_mitigation.utils import executor_expectation_values

gamma, gamma_post = mit["gammas"]
basis_map = {Pauli("X" * n_data): [SparsePauliOp(o) for o in observables]}
rescale = dict(zip(observables, mit["trex_rescale"], strict=True))


def evs(bits, basis_map, **kwargs):
    """Per-site (means, standard errors) from boolean shot data `bits` of shape (rands, 1, shots, n_data)."""
    out = executor_expectation_values(
        bits, basis_map, None, avg_axis=(0, 1), **kwargs
    )
    return np.array([m for m, _ in out]).ravel(), np.sqrt(
        [v for _, v in out]
    ).ravel()


def unpack(mit, prefix, n_data):
    """Restore `mit[f"data_{prefix}"]` from packed bytes to booleans of shape (rands, 1, shots, n_data)."""
    return np.unpackbits(mit[f"data_{prefix}"], axis=-1)[..., :n_data].astype(
        bool
    )


unmit_tw, unmit_tw_err = evs(
    mit["data_tw"], basis_map, measurement_flips=mit["flips_tw"]
)
ed_tw, ed_tw_err = evs(
    mit["data_tw"],
    basis_map,
    measurement_flips=mit["flips_tw"],
    postselect_mask=mit["mask_tw"],
    rescale_factors=rescale,
)
post, post_err = evs(
    unpack(mit, "post", n_data),
    basis_map,
    measurement_flips=mit["flips_post"],
    pauli_signs=mit["signs_post"],
    postselect_mask=mit["mask_post"],
    rescale_factors=rescale,
)
pec, pec_err = evs(
    unpack(mit, "van", n_data),
    basis_map,
    measurement_flips=mit["flips_van"],
    pauli_signs=mit["signs_van"],
    postselect_mask=mit["mask_van"],
    rescale_factors=rescale,
)

# effective ED+PEC overhead measured from the data: accepted shots per signed accepted shot
signs_post = 1 - 2 * (np.squeeze(mit["signs_post"]).sum(axis=-1) % 2)
gamma_eff = (
    mit["mask_post"].sum()
    / (signs_post * np.squeeze(mit["mask_post"]).sum(axis=-1)).sum()
)

print(
    f"gamma PEC {gamma:.2f} | gamma QED+PEC {gamma_post:.2f} (model) / {gamma_eff:.2f} (data) | "
    f"QED survival {mit['mask_tw'].mean():.3f} | QED+PEC survival {mit['mask_post'].mean():.3f} | "
    f"mean QED+PEC err {post_err.mean():.4f}"
)
plot_final(
    obs_exact,
    (unmit_tw, unmit_tw_err),
    (ed_tw, ed_tw_err),
    (pec, pec_err),
    (post, post_err),
    (gamma, gamma_post),
    x_labels(layout, n_data),
    "",
)

Output:

gamma PEC 10.08 | gamma QED+PEC 2.52 (model) / 2.51 (data) | QED survival 0.304 | QED+PEC survival 0.305 | mean QED+PEC err 0.0037
Output of the previous code cell
plot_convergence(mit, obs_exact, n_data)

Output:

Output of the previous code cell
Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.