Postselekce s kontrolami nemarkovovských chyb
Tento návod vysvětluje, co jsou kontroly nemarkovovských chyb a jak je implementovat v softwaru pomocí qiskit-mitigation. Začneš malým dvouqubitovým příkladem, abys kontroly zvizualizoval a probral, jak fungují. Až si vybuduješ určitou intuici, vyzkoušíš 20qubitový příklad, ve kterém kontroly implementuješ a porovnáš různé strategie postselekce.
Co jsou kontroly nemarkovovských chyb?
Kontroly nemarkovovských chyb jsou testy založené na měření, zda je stav qubitu pravděpodobně poškozen nemarkovovským zdrojem chyb. Test se provádí umístěním hradla pro převrácení bitu s prodlouženou dobou trvání (xslow nebo série hradel rx s úhlem ) na každý qubit na začátku obvodu, následovaného běžným XGate a měřením, aby se ověřilo, že se stav úspěšně převrátil . Tento test lze provést také na konci obvodu po každém koncovém měření, aby se ověřilo, že zařízení dokáže úspěšně převrátit naměřený stav qubitu z . Vzorky spojené s neúspěšnými kontrolami lze považovat za nespolehlivé a postselekce pouze vzorků, které projdou všemi kontrolami, může zvýšit fidelitu zašuměného rozdělení.
Strategie postselekce na základě kontrol nemarkovovských chyb
-
Postselekce založená na uzlech: Ponechají se pouze vzorky, u nichž každý qubit prošel svou kontrolou (svými kontrolami). Je to nejpřísnější strategie a obecně vede k vyřazení více vzorků.
-
Postselekce založená na hranách: Pro každou hranu v grafu propojení qubitů se zajistí, že alespoň jeden qubit kontrolou (kontrolami) projde. Tato strategie je shovívavější; pokud qubit kontrolou neprojde, vzorek může být přesto ponechán, pokud všichni jeho sousedé své kontroly splní.
Spektátorové qubity
Lze zadat „spektátorové“ pomocné qubity, které sousedí s qubity použitými ke spuštění původního obvodu. Na tyto spektátory lze umístit kontroly nemarkovovských chyb, což poskytuje větší pokrytí proti nemarkovovskému šumu, který mají tyto kontroly detekovat.
Část 1. Vizualizuj kontroly nemarkovovských chyb
Než techniku předvedeš na QPU, vytvoříš malý dvouqubitový obvod a přidáš kontroly, abys zvizualizoval, jak funguje. Začni Bellovým stavem transpilovaným pro QPU backend ibm_kingston.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
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")
Přidej několik kontrol nemarkovovských chyb na začátek obvodu. Kontroly budou umístěny pouze na qubity, které jsou zakončeny měřením. Tyto kontroly ověřují, že zařízení dokáže úspěšně převrátit počáteční stav qubitu , a měření těchto kontrol se čtou do nového klasického registru nazvaného 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)
Podobně můžeš přidat kontroly na konec obvodu po každém koncovém měření. Tyto kontroly ověřují, že zařízení dokáže převrátit stav qubitu z , a jejich měření se čtou do nového klasického registru nazvaného 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)
Jak bylo zmíněno v úvodu, můžeš použít pomocné qubity (ancilla), které sousedí s datovými qubity, jako „diváky“ (spectators) a získat tak větší pokrytí chyb. Přidej kontroly diváků před obvodem a po něm. Vidíš, že protože fyzický qubit 2 na ibm_kingston sousedí s datovým qubitem 1, byl přidán jako divák a byly k němu přidány kontroly před obvodem i po něm. Měření diváků byla přidána do nových klasických registrů nazvaných spec_pre a spec.
Takový obvod obsahuje všechny možné nemarkovovské kontroly chyb, takže vzorkování z tohoto obvodu uživatelům umožňuje provádět postselekci na základě libovolné kombinace kontrol [před/po] obvodu a strategie postselekce založené na [uzlech/hranách]. V části 2 tohoto průvodce se podívej, jak to funguje na kvantovém hardwaru.
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)

Část 2: Ukázka nemarkovovských kontrol chyb na QPU a porovnání strategií postselekce
Nyní, když rozumíš tomu, co nemarkovovské kontroly chyb jsou a jak se implementují v softwaru, můžeš spustit větší příklad na QPU a vidět, jak to funguje. Nejprve připrav obvod. Volba obvodu není příliš důležitá; potřebuješ jen obvod, jehož výstup lze snadno ověřit. Zde jsme zvolili 20qubitový jednorozměrný zrcadlený Isingův obvod. Tento obvod je na hardwaru netriviální k provedení a měl by být ovlivněn nemarkovovskými chybami, což dává příležitost ukázat účinnost nemarkovovských kontrol chyb. Zrcadlená struktura nám navíc umožňuje triviálně porovnat zašuměné výpočty střední hodnoty s přesnými hodnotami.
V tomto experimentu počítáme střední hodnoty pozorovatelných. Ani volba pozorovatelné není důležitá, proto jednoduše zvolíme průměrnou jednomístnou magnetizaci .
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)
two-qubit depth: 24
total depth: 85
Nyní přidáme kontroly před obvodem a po něm na každém qubitu, včetně sedmi sousedních qubitů-diváků. Když do obvodu přidáš všechny možné kontroly, můžeš se při následném zpracování rozhodnout, kterou strategii postselekce použít.
# 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])
cregs: [('meas', 20), ('meas_pre', 20), ('spec_pre', 7), ('meas_ps', 20), ('spec', 7), ('spec_ps', 7)]
Použij primitivu Executor k vzorkování obvodu obsahujícího kontroly.
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()}")
job id: d9badrm6hjac73ffe31g
status: QUEUED
Třída PostSelector zná strukturu creg kontrol, takže dokáže přiřadit každý bit ve vzorcích k příslušnému měření v obvodu. Její metodu compute_mask lze použít k vytvoření masky nad posloupností vzorků, a to na základě strategie postselekce (uzly nebo hrany) a typu nemarkovovských kontrol chyb, které se mají sledovat (před obvodem nebo po něm). Chceš-li spojit strategie před obvodem a po něm do jediné masky, můžeš jejich masky spočítat nezávisle a poté je zkombinovat operací AND. Zde použijeme PostSelector k vytvoření masek pro všechny různé strategie a prohlédneme si výsledky pro všechny dostupné permutace strategií.
Na vizualizaci níže vidíme, jak strategie postselekce ovlivňuje míru postselekce i kvalitu odpovědi. Podle očekávání vedly strategie zahrnující postselekci založenou na uzlech k nejnižším mírám postselekce. Strategie založené na hranách jsou shovívavější a mívají vyšší míry postselekce. Uživatelé by měli na svém problému vyzkoušet různé strategie a určit správnou rovnováhu mezi mírou postselekce a kvalitou vzorkovaného rozdělení. V tomto příkladu vedlo použití postselekce založené na uzlech s kontrolami před obvodem i po něm k nejlepší odpovědi s mírou postselekce .028%. Některé strategie nepřekonaly surový obvod. Také vidíš, že přidání kontrol, ale žádné jejich využití (sloupec označený „no selection“ v grafu níže), dopadá nejhůře.
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()
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)

