Sari la conținutul principal
Acest proiect este în versiune beta. Ai găsit o eroare? Deschide un issue pe GitHub. Ai o întrebare sau o idee? Întreabă în Discussions — ne-ar plăcea să aflăm părerea ta!
Cum este calitatea traducerii pe această pagină?

Postselecție cu verificări ale erorilor non-markoviene

Acest ghid explică ce sunt verificările erorilor non-markoviene și cum să le implementezi în software cu qiskit-mitigation. Vei începe cu un exemplu mic, cu doi qubiți, pentru a vizualiza verificările și a discuta cum funcționează. După ce îți formezi o intuiție, vei încerca un exemplu cu 20 de qubiți în care implementezi verificări și compari diverse strategii de postselecție.

Ce sunt verificările erorilor non-markoviene?​

Verificările erorilor non-markoviene sunt teste bazate pe măsurare care stabilesc dacă starea unui qubit este probabil coruptă de o sursă de eroare non-markoviană. Testul se face plasând o poartă de inversare a bitului cu durată extinsă (xslow sau o serie de NN porți rx cu unghiul πN\frac{\pi}{N}) pe fiecare qubit la începutul unui circuit, urmată de o XGate obișnuită și de o măsurare, pentru a verifica dacă starea a fost inversată cu succes ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle. Acest test poate fi efectuat și la sfârșitul circuitului, după fiecare măsurătoare terminală, pentru a verifica dacă dispozitivul poate inversa cu succes starea măsurată a qubitului ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle. Eșantioanele asociate verificărilor eșuate pot fi considerate nesigure, iar postselecția doar a eșantioanelor care trec toate verificările poate crește fidelitatea distribuției cu zgomot.

Strategii de postselecție bazate pe verificări ale erorilor non-markoviene​

  • Postselecție pe noduri: postselectează doar eșantioanele pentru care fiecare qubit a trecut verificarea (verificările) sa. Aceasta este cea mai strictă strategie și, în general, duce la eliminarea mai multor eșantioane.

  • Postselecție pe muchii: pentru fiecare muchie din graful de cuplaj al qubiților, asigură-te că cel puțin un qubit trece verificarea (verificările). Această strategie este mai permisivă; dacă un qubit eșuează la o verificare, eșantionul poate fi păstrat totuși dacă toți vecinii săi trec verificările lor.

Qubiți spectator​

Poți specifica qubiți ancilla „spectator” adiacenți qubiților folosiți pentru a executa circuitul original. Verificările erorilor non-markoviene pot fi plasate pe acești spectatori, oferind o acoperire mai mare împotriva zgomotului non-markovian pe care aceste verificări sunt concepute să îl detecteze.

Partea 1. Vizualizează verificările erorilor non-markoviene​

Înainte de a demonstra tehnica pe un QPU, vei crea un circuit mic cu doi qubiți și vei adăuga verificări pentru a vizualiza cum funcționează. Începe cu o stare Bell transpilată pentru QPU-ul 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")

Output of the previous code cell

Adaugă câteva verificări ale erorilor non-markoviene la începutul circuitului. Verificările vor fi plasate doar pe qubiții care se termină cu o măsurătoare. Aceste verificări asigură că dispozitivul poate inversa cu succes starea inițială a qubitului ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle, iar măsurătorile acestor verificări sunt citite într-un registru clasic nou numit 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 of the previous code cell

În mod similar, poți adăuga verificări la sfârșitul circuitului, după fiecare măsurătoare terminală. Aceste verificări asigură că dispozitivul poate inversa starea qubitului ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle, iar măsurătorile lor sunt citite într-un registru clasic nou numit 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 of the previous code cell

După cum am menționat în introducere, poți folosi qubiți ancilla adiacenți qubiților de date drept „spectatori” pentru a oferi o acoperire mai bună a erorilor. Adaugă verificări spectator înainte și după circuit. Poți vedea că, deoarece qubitul fizic 2 de pe ibm_kingston este adiacent qubitului de date 1, a fost adăugat ca spectator, iar verificările dinaintea și de după circuit au fost adăugate pentru el. Măsurătorile spectatorilor au fost adăugate în registre clasice noi, numite spec_pre și spec.

Un circuit ca acesta conține toate verificările posibile ale erorilor non-Markoviene, așa că eșantionarea din acest circuit le oferă utilizatorilor posibilitatea de a face postselecție pe baza oricărei combinații de verificări [înainte/după]-circuit și a unei strategii de postselecție bazate pe [noduri/muchii]. Consultă Partea 2 a acestui ghid pentru a vedea cum funcționează pe hardware cuantic.

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 of the previous code cell

Partea 2: Demonstrează verificările erorilor non-Markoviene pe un QPU și compară strategiile de postselecție​

Acum că ai înțeles ce sunt verificările erorilor non-Markoviene și cum sunt implementate în software, poți rula un exemplu mai mare pe un QPU și poți vedea cum funcționează. Mai întâi, pregătește un circuit. Alegerea circuitului nu este foarte importantă; ai nevoie doar de un circuit al cărui rezultat poate fi verificat cu ușurință. Aici alegem un circuit Ising oglindit 1D cu 20 de qubiți. Acest circuit este netrivial de executat pe hardware și ar trebui să fie afectat de erori non-Markoviene, ceea ce oferă ocazia de a demonstra eficiența verificărilor erorilor non-Markoviene. În plus, structura oglindită ne permite să comparăm trivial calculele zgomotoase ale valorii de așteptare cu valorile exacte.

În acest experiment calculăm valori de așteptare ale unor observabile. Nici alegerea observabilei OO nu este importantă, așa că alegem simplu magnetizarea medie pe un singur situs O=1N∑i⟨Zi⟩O=\frac{1}{N}\sum_i\langle Z_i\rangle.

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

Output of the previous code cell

Acum adăugăm verificări înainte și după circuit pe fiecare qubit, inclusiv cei șapte qubiți spectator adiacenți. Când adaugi toate verificările posibile la un circuit, poți decide ce strategie de postselecție să folosești în timpul postprocesării.

# 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)]

Folosește primitiva Executor pentru a eșantiona circuitul care conține verificările.

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

Clasa PostSelector cunoaște structura creg a verificărilor, deci poate asocia fiecare bit din eșantioane cu măsurătoarea de circuit corespunzătoare. Metoda sa compute_mask poate fi folosită pentru a crea o mască peste o secvență de eșantioane, dată fiind strategia de postselecție (nod sau muchie) și tipul de verificări ale erorilor non-Markoviene de analizat (înainte sau după circuit). Pentru a combina strategiile dinaintea și de după circuit într-o singură mască, măștile lor pot fi calculate independent și apoi combinate cu operația AND. Aici folosim PostSelector pentru a crea măști pentru toate strategiile și analizăm rezultatele folosind fiecare permutare de strategii disponibilă.

În vizualizarea de mai jos vedem cum influențează strategia de postselecție rata de postselecție, precum și calitatea răspunsului. Așa cum ne așteptam, strategiile care implică postselecție bazată pe noduri au dus la cele mai mici rate de postselecție. Strategiile bazate pe muchii sunt mai îngăduitoare și tind să ducă la rate de postselecție mai mari. Utilizatorii ar trebui să experimenteze cu diversele strategii pe problema lor pentru a determina echilibrul potrivit între rata de postselecție și calitatea distribuției eșantionate. În acest exemplu, folosirea postselecției bazate pe noduri cu verificările dinaintea și de după circuit a dat cel mai bun răspuns, cu o rată de postselecție de .028%. Unele dintre strategii nu au depășit circuitul brut. Poți vedea și că adăugarea verificărilor fără a folosi vreuna dintre ele (bara etichetată „no selection” în graficul de mai jos) are cele mai slabe rezultate.

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)

Output of the previous code cell