Nachauswahl mit nicht-markovschen Fehlerprüfungen
In diesem Leitfaden wird erläutert, was nicht-markovsche Fehlerprüfungen sind und wie man sie in Software mit … implementiert qiskit-mitigation. Sie beginnen mit einem kleinen Beispiel mit zwei Qubits, um die Prüfungen zu veranschaulichen und zu erläutern, wie sie funktionieren. Sobald Sie ein gewisses Gespür dafür entwickelt haben, werden Sie ein Beispiel mit 20 Qubits ausprobieren, bei dem Sie Prüfungen implementieren und verschiedene Postselektionsstrategien vergleichen.
Was sind nicht-Markovsche Fehlerprüfungen?
Nicht-Markovsche Fehlerprüfungen sind messungsbasierte Tests, mit denen überprüft wird, ob der Zustand eines Qubits wahrscheinlich durch eine nicht-Markovsche Fehlerquelle beeinträchtigt ist. Dieser Test wird durchgeführt, indem zu Beginn einer Schaltung an jedem Qubit ein Bit-Flip-Gatter mit verlängerter Dauer (xslow oder eine Reihe von -Gattern rx mit Winkel ) angelegt wird, gefolgt von einem regulären Gatter und XGate einer Messung, um sicherzustellen, dass der Zustand erfolgreich von umgedreht wurde. Dieser Test kann auch am Ende der Schaltung nach jeder abschließenden Messung durchgeführt werden, um sicherzustellen, dass das Gerät den gemessenen Zustand des Qubits erfolgreich von umdrehen kann. Proben, bei denen die Prüfungen fehlgeschlagen sind, können als unzuverlässig angesehen werden, und die Nachauswahl nur jener Proben, die alle Prüfungen bestehen, kann die Genauigkeit der verrauschten Verteilung erhöhen.
Strategien zur Nachauswahl auf der Grundlage nicht-markovscher Fehlerprüfungen
- Knotenbasierte Nachauswahl : Es werden nur diejenigen Stichproben nachgewählt, bei denen jedes Qubit die jeweilige(n) Prüfung(en) bestanden hat. Dies ist die strengste Strategie und führt in der Regel dazu, dass mehr Stichproben verworfen werden.
- Kantenbasierte Nachauswahl : Stelle für jede Kante im Kopplungsgraphen der Qubits sicher, dass mindestens ein Qubit die Prüfung(en) besteht. Diese Strategie ist großzügiger: Wenn ein Qubit eine Prüfung nicht besteht, kann das Muster dennoch beibehalten werden, sofern alle seine Nachbarn ihre Prüfungen bestehen.
Beobachter-Qubits
Man kann „Zuschauer“-Ancilla-Qubits festlegen, die an die Qubits angrenzen, die zur Ausführung der ursprünglichen Schaltung verwendet werden. Auf diese „Zuschauer“ können nicht-markovsche Fehlerprüfungen angewendet werden, wodurch ein umfassenderer Schutz vor dem nicht-markovschen Rauschen geboten wird, auf dessen Erkennung diese Prüfungen ausgelegt sind.
Teil 1. Visualisierung nicht-Markovscher Fehlerprüfungen
Bevor Sie die Technik auf einer QPU demonstrieren, erstellen Sie eine kleine Schaltung mit zwei Qubits und fügen Prüfmechanismen hinzu, um deren Funktionsweise zu veranschaulichen. Beginnen Sie mit einem Bell-Zustand, der auf die ibm_kingston Backend-QPU transpiliert wurde.
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:
Fügen Sie am Anfang der Schaltung einige nicht-Markovsche Fehlerprüfungen ein. Es werden nur Überprüfungen an Qubits durchgeführt, die mit einer Messung abgeschlossen werden. Diese Prüfungen stellen sicher, dass das Gerät den Anfangszustand des Qubits erfolgreich von „ “ umkehren kann, und die Messwerte dieser Prüfungen werden in ein neues klassisches Register namens … ausgelesen 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:
Ebenso können Sie nach jeder Messung an einem Anschluss Prüfungen am Ende des Stromkreises einfügen. Diese Prüfungen stellen sicher, dass das Gerät den Zustand des Qubits von „ “ umschalten kann, und ihre Messwerte werden in einem neuen klassischen Register namens … ausgelesen 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:
Wie in der Einleitung erwähnt, können Sie Ancilla-Qubits, die an die Daten-Qubits angrenzen, als „Beobachter“ einsetzen, um eine umfassendere Fehlerabdeckung zu erzielen. Fügen Sie Kontrollen durch Zuschauer vor und nach der Rennrunde hinzu. Wie Sie sehen können, wurde das physikalische Qubit auf 2 , da es neben dem Daten-Qubit liegt ibm_kingston``1 , als Beobachter hinzugefügt, und es wurden Vor- und Nachprüfungen des Schaltkreises hinzugefügt. Die Zuschauerzahlen wurden in neue klassische Register namens spec_pre und aufgenommen spec.
Ein Schaltkreis wie dieser enthält alle möglichen nicht-markovschen Fehlerprüfungen, sodass die Stichprobenentnahme aus diesem Schaltkreis den Anwendern die Möglichkeit bietet, eine Nachauswahl auf der Grundlage einer beliebigen Kombination aus [pre/post]Schaltkreisprüfungen und einer darauf [node/edge]basierenden Nachauswahlstrategie vorzunehmen. In Teil 2 dieses Leitfadens erfahren Sie, wie dies auf Quantenhardware funktioniert.
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:
Teil 2: Demonstration nicht-markovscher Fehlerprüfungen auf einer QPU und Vergleich von Postselektionsstrategien
Nachdem Sie nun ein Verständnis dafür gewonnen haben, was nicht-Markovsche Fehlerprüfungen sind und wie sie in Software umgesetzt werden, können Sie ein umfangreicheres Beispiel auf einer QPU ausführen und sehen, wie es funktioniert.
Bereiten Sie zunächst eine Schaltung vor. Die Wahl der Schaltung spielt keine große Rolle; du brauchst lediglich eine Schaltung, bei der du den Ausgang leicht überprüfen kannst. Hier wählen wir einen gespiegelten Ising-Schaltkreis mit 20 Qubits und der Konfiguration „ 1D “. Die Ausführung dieser Schaltung auf Hardware ist nicht trivial und dürfte von nicht-markovschen Fehlern beeinflusst werden, was die Gelegenheit bietet, die Wirksamkeit der nicht-markovschen Fehlerprüfungen zu demonstrieren. Außerdem ermöglicht uns die gespiegelte Struktur, die Berechnungen der verrauschten Erwartungswerte auf einfache Weise mit den exakten Werten zu vergleichen.
In diesem Experiment berechnen wir die Erwartungswerte der beobachtbaren Größen. Die Wahl der zu beobachtenden ist ebenfalls nicht wichtig, daher wählen wir einfach die durchschnittliche Magnetisierung an einem einzelnen Ort .
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
Nun führen wir für jedes Qubit, einschließlich der sieben benachbarten Beobachter-Qubits, Überprüfungen vor und nach dem Schaltvorgang durch. Wenn Sie alle möglichen Prüfungen in einen Schaltkreis einfügen, können Sie entscheiden, welche Nachauswahlstrategie bei der Nachbearbeitung verwendet werden soll.
# 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)]
Verwenden Sie die Primitive Executor , um die Schaltung mit den Prüfungen abzutasten.
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
Die Klasse PostSelector kennt die Struktur creg der Prüfungen, sodass sie jedes Bit in den Stichproben der entsprechenden Schaltungsmessung zuordnen kann. Mit dieser Methode compute_mask lässt sich eine Maske über eine Abfolge von Stichproben erstellen, wobei die Postselektionsstrategie (Knoten oder Kante) und die Art der zu prüfenden nicht-Markovschen Fehlerprüfungen (vor oder nach dem Schaltkreis) vorgegeben sind. Um Strategien vor und nach dem Schaltkreis in einer einzigen Maske zu kombinieren, können die Masken unabhängig voneinander berechnet und anschließend mit der Operation AND kombiniert werden. Hier verwenden wir das, PostSelector um Masken für alle verschiedenen Strategien zu erstellen, und betrachten die Ergebnisse unter Berücksichtigung aller möglichen Permutationen der Strategien.
In der folgenden Visualisierung sehen wir, wie sich die Postselektionsstrategie auf die Postselektionsrate sowie auf die Qualität der Antwort auswirkt. Wie erwartet führten die Strategien mit knotenbasierter Nachselektion zu den niedrigsten Nachselektionsraten. Die auf den Rändern basierenden Strategien sind großzügiger und führen tendenziell zu höheren Nachselektionsraten. Die Nutzer sollten mit den verschiedenen Strategien für ihr Problem experimentieren, um das richtige Gleichgewicht zwischen Postselektionsrate und Qualität der Stichprobenverteilung zu ermitteln. In diesem Beispiel führte die Anwendung der knotenbasierten Nachselektion in Verbindung mit den Prüfungen vor und nach dem Schaltvorgang zum besten Ergebnis mit einer Nachselektionsrate von .028%. Einige der Strategien erzielten keine bessere Performance als der Rohindex. Man sieht auch, dass das Hinzufügen der Kontrollkästchen, ohne eines davon zu verwenden (Balken mit der Bezeichnung „Keine Auswahl“ in der folgenden Grafik), das schlechteste Ergebnis liefert.
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)