Skip to main content
IBM Quantum Platform

Post-sélection avec contrôles d'erreurs non markoviens

Ce guide explique ce que sont les contrôles d'erreurs non markoviens et comment les mettre en œuvre dans un logiciel à l'aide de qiskit-mitigation. Vous commencerez par un petit exemple à deux qubits afin de visualiser les vérifications et d'expliquer leur fonctionnement. Une fois que vous aurez acquis une certaine intuition, vous vous exercerez sur un exemple à 20 qubits dans lequel vous mettrez en œuvre des contrôles et comparerez différentes stratégies de post-sélection.

Qu'est-ce qu'un contrôle d'erreurs non markovien?

Les contrôles d'erreurs non markoviennes sont des tests basés sur des mesures visant à déterminer si l'état d'un qubit est susceptible d'avoir été altéré par une source d'erreur non markovienne. Ce test consiste à placer une porte de basculement de bit à durée prolongée (xslow ou une série de portes rx « NN » avec une πN\frac{\pi}{N} d’angle) sur chaque qubit au début d’un circuit, puis à effectuer XGate une mesure standard pour vérifier que l’état a bien basculé à partir de ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle. Ce test peut également être effectué à la fin du circuit, après chaque mesure terminale, afin de s’assurer que le dispositif est capable de faire basculer correctement l’état mesuré du qubit à partir de ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle. Les échantillons associés à des contrôles infructueux peuvent être considérés comme non fiables, et la post-sélection des seuls échantillons ayant passé tous les contrôles peut améliorer la fidélité de la distribution bruyante.

Stratégies de post-sélection fondées sur des contrôles d'erreurs non markoviens

  • Post-sélection par nœud : ne post-sélectionner que les échantillons pour lesquels chaque qubit a réussi son ou ses contrôles. Il s'agit de la stratégie la plus stricte, qui entraîne généralement l'élimination d'un plus grand nombre d'échantillons.
  • Post-sélection basée sur les arêtes : pour chaque arête du graphe de couplage des qubits, s'assurer qu'au moins un qubit satisfait aux critères de vérification. Cette stratégie est plus souple : si un qubit échoue à un contrôle, l'échantillon peut tout de même être conservé si tous ses voisins réussissent leurs contrôles.

Qubits spectateurs

On peut définir des qubits ancilla « spectateurs » adjacents aux qubits utilisés pour exécuter le circuit d’origine. Des contrôles d'erreurs non markoviens peuvent être appliqués à ces spectateurs, offrant ainsi une meilleure protection contre le bruit non markovien que ces contrôles sont censés détecter.

Partie 1. Visualisation des contrôles d'erreurs non markoviens

Avant de faire la démonstration de cette technique sur un QPU, vous allez créer un petit circuit à deux qubits et y ajouter des contrôles afin de visualiser son fonctionnement. Commencez par un état de Bell transpilé vers le QPU de destination ibm_kingston .

from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.backend("ibm_kingston")

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
qc_isa = generate_preset_pass_manager(
    optimization_level=0, backend=backend
).run(qc)
qc_isa.draw("mpl")

Output:

Output of the previous code cell

Ajoutez quelques contrôles d'erreurs non markoviens au début du circuit. Les vérifications ne porteront que sur les qubits dont l'état est déterminé par une mesure. Ces vérifications garantissent que le dispositif est capable de faire basculer correctement l'état initial du qubit, qui est de type « ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle », et les mesures issues de ces vérifications sont lues dans un nouveau registre classique appelé meas_pre.

from qiskit.transpiler import PassManager
from qiskit_mitigation.postselection.passes import (
    AddPreCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager([AddPreCircuitNonMarkovianErrorChecks()]).run(
    qc_isa
)
qc_w_checks.draw("mpl", idle_wires=False)

Output:

Output of the previous code cell

De la même manière, vous pouvez ajouter des contrôles à la fin du circuit après chaque mesure aux bornes. Ces vérifications permettent de s'assurer que le dispositif est capable de faire basculer l'état du qubit de l' ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle et que leurs mesures sont lues dans un nouveau registre classique appelé meas_ps.

from qiskit_mitigation.postselection.passes import (
    AddPostCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager([AddPostCircuitNonMarkovianErrorChecks()]).run(
    qc_w_checks
)
qc_w_checks.draw("mpl", idle_wires=False)

Output:

Output of the previous code cell

Comme indiqué dans l'introduction, vous pouvez utiliser des qubits auxiliaires adjacents aux qubits de données en tant que « spectateurs » afin d'assurer une meilleure couverture des erreurs. Ajouter des contrôles effectués par les spectateurs avant et après le circuit. Comme vous pouvez le constater, le qubit physique 2 sur ibm_kingston étant adjacent au qubit de données 1, il a été ajouté en tant que « spectateur », et des vérifications avant et après le circuit lui ont été associées. Les mesures des spectateurs ont été ajoutées à de nouveaux registres classiques appelés et spec_pre spec.

Un circuit de ce type intègre tous les contrôles d'erreurs non markoviens possibles; ainsi, l'échantillonnage à partir de ce circuit permet aux utilisateurs d'effectuer une post-sélection en s'appuyant sur n'importe quelle combinaison de contrôles du [pre/post]circuit et de stratégie de post-sélection [node/edge]basée sur celui-ci. Consultez la deuxième partie de ce guide pour découvrir comment cela fonctionne sur du matériel quantique.

from qiskit_mitigation.postselection.passes import (
    AddSpectatorPostCircuitNonMarkovianErrorChecks,
    AddSpectatorPreCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager(
    [
        AddSpectatorPreCircuitNonMarkovianErrorChecks(
            coupling_map=backend.coupling_map
        ),
        AddSpectatorPostCircuitNonMarkovianErrorChecks(
            coupling_map=backend.coupling_map
        ),
    ]
).run(qc_w_checks)
qc_w_checks.draw("mpl", idle_wires=False, fold=-1)

Output:

Output of the previous code cell

Partie 2 : Démonstration de contrôles d'erreurs non markoviens sur un QPU et comparaison des stratégies de post-sélection

Maintenant que vous comprenez ce que sont les contrôles d'erreurs non markoviens et comment ils sont mis en œuvre dans les logiciels, vous pouvez exécuter un exemple plus complet sur un QPU et observer son fonctionnement.

Commencez par monter un circuit. Le choix du circuit n'a pas beaucoup d'importance; il suffit d'un circuit dont on puisse facilement vérifier la sortie. Nous choisissons ici un circuit d'Ising symétrique de 20 qubits, de type « 1D ». L'exécution de ce circuit sur du matériel n'est pas aisée et devrait être affectée par des erreurs non markoviennes, ce qui permet de démontrer l'efficacité des contrôles d'erreurs non markoviennes. De plus, la structure symétrique nous permet de comparer très facilement les calculs de la valeur attendue, entachés de bruit, aux valeurs exactes.

Dans cette expérience, nous calculons les valeurs attendues des grandeurs observables. Le choix de l' OO observable n'a pas non plus d'importance; nous choisissons donc simplement l' O=1N∑i⟨Zi⟩O=\frac{1}{N}\sum_i\langle Z_i\rangle de magnétisation moyenne par site.

import numpy as np
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap

num_qubits = 20
initial_layout = initial_layout = [
    124,
    125,
    126,
    127,
    128,
    129,
    130,
    131,
    132,
    133,
    134,
    135,
    139,
    155,
    154,
    153,
    152,
    151,
    150,
    149,
][:num_qubits]
coupling_map = CouplingMap.from_line(num_qubits, bidirectional=False)

# Num Trotter steps and `rx` angle
num_steps = 6
theta_rx = np.pi / 6

# Average single-site magnetization
observable = (
    SparsePauliOp(
        [
            "I" * iq + "Z" + "I" * (num_qubits - iq - 1)
            for iq in range(num_qubits)
        ]
    )
    / num_qubits
)

# Create the Trotter circuit
circuit = QuantumCircuit(num_qubits)
edges = CouplingMap.from_line(num_qubits, bidirectional=False).get_edges()
edges_a = [e for e in edges if e[0] % 2 == 0]
edges_b = [e for e in edges if e[0] % 2 == 1]
for _ in range(num_steps):
    circuit.rx(theta_rx, [i for i in range(num_qubits)])
    for edge in edges_a:
        circuit.cz(edge[0], edge[1])
    for edge in edges_b:
        circuit.cz(edge[0], edge[1])
circuit = circuit.compose(circuit.inverse())
circuit.measure_all()

# Transpile circuit to backend
pm = generate_preset_pass_manager(
    optimization_level=0, backend=backend, initial_layout=initial_layout
)
isa_circuit = pm.run(circuit)
print(
    f"two-qubit depth: {isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)}"
)
print(f"total depth:     {isa_circuit.depth()}")
isa_circuit.draw("mpl", fold=-1, idle_wires=False)

Output:

two-qubit depth: 24
total depth:     85
Output of the previous code cell

Nous ajoutons désormais des vérifications avant et après le circuit sur chaque qubit, y compris les sept qubits « spectateurs » adjacents. Lorsque vous ajoutez toutes les vérifications possibles à un circuit, vous pouvez choisir la stratégie de post-sélection à utiliser lors du post-traitement.

# Use all non-Markovian error checks so we can compare all strategies in post-processing.
combined = PassManager(
    [
        AddPreCircuitNonMarkovianErrorChecks(x_pulse_type="rx"),
        AddSpectatorPreCircuitNonMarkovianErrorChecks(
            backend.coupling_map, x_pulse_type="rx"
        ),
        AddPostCircuitNonMarkovianErrorChecks(x_pulse_type="rx"),
        AddSpectatorPostCircuitNonMarkovianErrorChecks(
            backend.coupling_map, x_pulse_type="rx"
        ),
    ]
).run(isa_circuit)
combined_isa = generate_preset_pass_manager(
    optimization_level=0, backend=backend
).run(combined)

print("cregs:", [(c.name, c.size) for c in combined_isa.cregs])

Output:

cregs: [('meas', 20), ('meas_pre', 20), ('spec_pre', 7), ('meas_ps', 20), ('spec', 7), ('spec_ps', 7)]

Utilisez la primitive Executor pour échantillonner le circuit contenant les vérifications.

from qiskit_ibm_runtime import Executor
from samplomatic.quantum_program import QuantumProgram

# Raw and combined-checks circuits in a single 100k-shot program so they share calibration.
program = QuantumProgram(shots=100_000)
program.append_circuit_item(isa_circuit)  # raw circuit, no checks
program.append_circuit_item(
    combined_isa
)  # combined pre + post non-Markovian error checks
job = Executor(backend).run(program)
print(f"job id: {job.job_id()}")
print(f"status: {job.status()}")

Output:

job id: d9badrm6hjac73ffe31g
status: QUEUED

La classe PostSelector connaît la structure creg des contrôles; elle peut donc associer chaque bit des échantillons à la mesure correspondante effectuée sur le circuit. Cette méthode compute_mask permet de créer un masque sur une séquence d'échantillons, en fonction de la stratégie de post-sélection (nœud ou arête) et du type de contrôles d'erreurs non markoviennes à examiner (avant ou après le circuit). Pour regrouper les stratégies pré-circuit et post-circuit dans un seul masque, il est possible de calculer ces masques indépendamment, puis de les combiner à l'aide de l'opération AND . Ici, nous utilisons le PostSelector pour créer des masques pour l'ensemble des différentes stratégies et nous analysons les résultats en testant toutes les permutations possibles de ces stratégies.

Dans la visualisation ci-dessous, on constate l'influence de la stratégie de post-sélection sur le taux de post-sélection, ainsi que sur la qualité de la réponse. Comme on pouvait s'y attendre, les stratégies impliquant une post-sélection par nœud ont donné lieu aux taux de post-sélection les plus faibles. Les stratégies basées sur les marges sont plus souples et ont tendance à se traduire par des taux de post-sélection plus élevés. Les utilisateurs devraient tester les différentes stratégies sur leur problème afin de déterminer le juste équilibre entre le taux de post-sélection et la qualité de la distribution échantillonnée. Dans cet exemple, le recours à la post-sélection par nœuds, associé aux vérifications avant et après le circuit, a permis d'obtenir le meilleur résultat, avec un taux de post-sélection de .028%. Certaines de ces stratégies n'ont pas obtenu de meilleurs résultats que le circuit brut. On constate également que le fait d'ajouter les cases à cocher sans en utiliser aucune (barre intitulée « aucune sélection » dans le graphique ci-dessous) donne les pires résultats.

import matplotlib.pyplot as plt
from qiskit_mitigation.postselection import PostSelector

selector = PostSelector.from_circuit(combined_isa, backend.coupling_map)

exec_results = job.result()
# The Executor returns numpy arrays keyed by classical-register name, so each result item is
# already the {creg_name: ndarray} mapping that compute_mask expects.
raw_bits = exec_results[0]["meas"]  # raw circuit, no checks
bits = exec_results[1]  # combined pre+post circuit

meas_bits = bits["meas"]
total = meas_bits[..., 0].size


def magnetization(b, mask=None):
    """Average single-site magnetization over the (optionally masked) measurement bits."""
    kept = b if mask is None else b[mask]
    return (1 - 2 * kept.astype(int)).mean()  # bit 0 -> +1, bit 1 -> -1


rows = []


def report(label, mask=None, b=meas_bits):
    """Record and print the magnetization and kept fraction for one selection strategy."""
    kept = b[..., 0].size if mask is None else int(mask.sum())
    ev = magnetization(b, mask)
    rows.append((label, ev, kept / total))
    print(f"{label:<24}: <m> = {ev:+.4f}   (kept {kept:>6}/{total})")


# Compare all possible postselection strategies
report("raw circuit", b=raw_bits)
report("no selection")
for s in ("node", "edge"):
    report(f"pre-only ({s})", selector.compute_mask(bits, s, mode="pre"))
for s in ("node", "edge"):
    report(f"post-only ({s})", selector.compute_mask(bits, s, mode="post"))
for pre_s in ("node", "edge"):
    for post_s in ("node", "edge"):
        keep = selector.compute_mask(
            bits, pre_s, mode="pre"
        ) & selector.compute_mask(bits, post_s, mode="post")
        report(f"pre={pre_s} + post={post_s}", keep)

labels = [r[0] for r in rows]
evs = [r[1] for r in rows]
fracs = [r[2] for r in rows]
x = range(len(labels))

fig, ax1 = plt.subplots(figsize=(11, 5))

# Bars: expectation value per selection strategy.
bar_colors = ["firebrick"] + ["steelblue"] * (len(evs) - 1)
ax1.bar(x, evs, color=bar_colors, alpha=0.85)
ax1.axhline(1.0, color="green", ls=":", lw=1.5, label="exact <m>")
ax1.axhline(rows[0][1], color="firebrick", ls="--", lw=1, label="raw <m>")
ax1.set_ylabel("<m>  (avg single-site magnetization)", color="steelblue")
ax1.tick_params(axis="y", labelcolor="steelblue")
ax1.set_xticks(list(x))
ax1.set_xticklabels(labels, rotation=45, ha="right")
ax1.legend(loc="upper right")

# Line: fraction of shots retained (right axis).
ax2 = ax1.twinx()
ax2.plot(x, fracs, "o-", color="darkorange")
ax2.set_ylabel("kept fraction", color="darkorange")
ax2.tick_params(axis="y", labelcolor="darkorange")
ax2.set_ylim(0, 1.05)

ax1.set_title("Selection strategy: expectation value vs shots retained")
fig.tight_layout()
plt.show()

Output:

raw circuit             : <m> = +0.5791   (kept 100000/100000)
no selection            : <m> = +0.5211   (kept 100000/100000)
pre-only (node)         : <m> = +0.6026   (kept   1357/100000)
pre-only (edge)         : <m> = +0.5637   (kept  13029/100000)
post-only (node)        : <m> = +0.5682   (kept   2254/100000)
post-only (edge)        : <m> = +0.5265   (kept  27514/100000)
pre=node + post=node    : <m> = +0.6393   (kept     28/100000)
pre=node + post=edge    : <m> = +0.5799   (kept    442/100000)
pre=edge + post=node    : <m> = +0.6006   (kept    317/100000)
pre=edge + post=edge    : <m> = +0.5521   (kept   3836/100000)
Output of the previous code cell
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.