Skip to main content
IBM Quantum Platform

Post-selezione con controlli di errore non markoviani

Questa guida spiega cosa sono i controlli di errore non markoviani e come implementarli in un software con qiskit-mitigation. Inizierai con un piccolo esempio a due qubit per illustrare i controlli e spiegare come funzionano. Una volta acquisita una certa dimestichezza, proverai un esempio a 20 qubit in cui implementerai dei controlli e metterai a confronto diverse strategie di post-selezione.

Cosa sono i controlli di errore non markoviani?

I controlli degli errori non markoviani sono test basati su misurazioni volti a verificare se lo stato di un qubit sia stato probabilmente compromesso da una fonte di errore non markoviana. Questo test viene eseguito inserendo un gate di inversione di bit a durata estesa (xslow o una serie di gate rx NN con angolo πN\frac{\pi}{N} ) su ciascun qubit all’inizio di un circuito, seguito da un gate regolare e XGate da una misurazione per verificare che lo stato sia stato invertito correttamente da ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle. Questo test può essere eseguito anche alla fine del circuito, dopo ogni misurazione terminale, per verificare che il dispositivo sia in grado di invertire correttamente lo stato misurato del qubit da ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle. I campioni associati a controlli falliti possono essere considerati inaffidabili, e la post-selezione dei soli campioni che superano tutti i controlli può aumentare la fedeltà della distribuzione rumorosa.

Strategie di post-selezione basate su controlli degli errori non markoviani

  • Post-selezione basata sui nodi : selezionare solo i campioni per i quali ogni qubit ha superato i relativi controlli. Questa è la strategia più rigorosa e, in genere, comporta lo scartamento di un numero maggiore di campioni.
  • Post-selezione basata sugli archi : per ogni arco nel grafo di accoppiamento dei qubit, assicurarsi che almeno un qubit superi i controlli. Questa strategia è più flessibile: se un qubit non supera un controllo, il campione può comunque essere mantenuto se tutti i suoi vicini superano i rispettivi controlli.

Qubit spettatori

È possibile specificare dei qubit ancilla "spettatori" adiacenti ai qubit utilizzati per eseguire il circuito originale. Su questi spettatori è possibile applicare controlli di errore non markoviani, garantendo una maggiore copertura contro il rumore non markoviano che tali controlli sono progettati per rilevare.

Parte 1. Visualizzazione dei controlli di errore non markoviani

Prima di illustrare la tecnica su una QPU, creerai un piccolo circuito a due qubit e aggiungerai dei controlli per visualizzarne il funzionamento. Si inizia con uno stato di Bell trasposto sul backend ibm_kingston QPU.

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

Aggiungere alcuni controlli di errore non markoviani all’inizio del circuito. Verranno effettuati controlli solo sui qubit che terminano con una misurazione. Questi controlli garantiscono che il dispositivo sia in grado di invertire correttamente lo stato iniziale del qubit da “ ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle ”, e i risultati delle misurazioni di questi controlli vengono letti in un nuovo registro classico denominato 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

Allo stesso modo, è possibile aggiungere controlli alla fine del circuito dopo ogni misurazione ai terminali. Questi controlli garantiscono che il dispositivo sia in grado di invertire lo stato del qubit da ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle, e i risultati delle misurazioni vengono letti in un nuovo registro classico denominato 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

Come accennato nell’introduzione, è possibile utilizzare i qubit ausiliari adiacenti ai qubit di dati come “spettatori” per garantire una maggiore copertura degli errori. Aggiungere controlli degli spettatori prima e dopo il circuito. Come si può notare, poiché il qubit fisico su 2 è ibm_kingston adiacente al qubit di dati 1, è stato aggiunto come spettatore e sono stati inseriti i controlli pre-circuito e post-circuito. Le misurazioni degli spettatori sono state aggiunte a nuovi registri classici denominati e spec_pre spec.

Un circuito come questo contiene tutti i possibili controlli di errore non markoviani, pertanto il campionamento da questo circuito offre agli utenti la possibilità di effettuare una post-selezione basata su qualsiasi combinazione di controlli del [pre/post]circuito e strategie di post-selezione [node/edge]basate su di esso. Consulta la Parte 2 di questa guida per scoprire come funziona sull'hardware quantistico.

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

Parte 2: Dimostrazione dei controlli di errore non markoviani su una QPU e confronto tra strategie di post-selezione

Ora che hai compreso cosa sono i controlli di errore non markoviani e come vengono implementati nel software, puoi eseguire un esempio più esteso su una QPU e vedere come funziona.

Per prima cosa, prepara un circuito. La scelta del circuito non è particolarmente importante; basta un circuito di cui sia possibile verificare facilmente l'uscita. In questo caso scegliamo un circuito di Ising speculare a 20 qubit, 1D. L'esecuzione di questo circuito su hardware non è banale e dovrebbe essere soggetta a errori non markoviani, il che offre l'opportunità di dimostrare l'efficacia dei controlli sugli errori non markoviani. Inoltre, la struttura speculare ci permette di confrontare facilmente i calcoli del valore atteso affetto da rumore con i valori esatti.

In questo esperimento stiamo calcolando i valori attesi delle grandezze osservabili. Neanche la scelta dell’ OO e osservabile è importante, quindi scegliamo semplicemente la magnetizzazione media per singolo sito 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)

Output:

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

Ora aggiungiamo dei controlli prima e dopo il circuito su ogni qubit, compresi i sette qubit “spettatori” adiacenti. Una volta aggiunti tutti i controlli possibili a un circuito, è possibile decidere quale strategia di post-selezione utilizzare durante la post-elaborazione.

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

Utilizza la primitiva Executor per campionare il circuito contenente i controlli.

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 conosce la struttura creg dei controlli, quindi è in grado di associare ciascun bit dei campioni alla misurazione del circuito corrispondente. Il suo metodo compute_mask può essere utilizzato per creare una maschera su una sequenza di campioni, data la strategia di post-selezione (nodo o bordo) e il tipo di controlli sugli errori non markoviani da esaminare (pre- o post-circuito). Per combinare le strategie pre-circuito e post-circuito in un’unica maschera, è possibile calcolare le rispettive maschere in modo indipendente e poi combinarle tramite l’operazione AND . Qui utilizziamo il PostSelector per creare maschere per tutte le diverse strategie e analizziamo i risultati utilizzando ogni possibile combinazione delle strategie disponibili.

Nella visualizzazione riportata di seguito, vediamo come la strategia di post-selezione influisca sul tasso di post-selezione, nonché sulla qualità della risposta. Come previsto, le strategie che prevedevano una post-selezione basata sui nodi hanno registrato i tassi di post-selezione più bassi. Le strategie basate sui bordi sono più permissive e tendono a determinare tassi di post-selezione più elevati. Gli utenti dovrebbero sperimentare le varie strategie applicandole al proprio problema per individuare il giusto equilibrio tra il tasso di post-selezione e la qualità della distribuzione campionaria. In questo esempio, l'utilizzo della post-selezione basata sui nodi, abbinata ai controlli pre- e post-circuito, ha portato alla risposta migliore con un tasso di post-selezione pari a .028%. Alcune delle strategie non hanno ottenuto risultati migliori rispetto al circuito grezzo. Si può inoltre notare che l'aggiunta dei controlli senza utilizzarne alcuno (barra contrassegnata con "nessuna selezione" nel grafico sottostante) dà i risultati peggiori.

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
Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.