Pós-seleção com verificações de erro não markovianas
Este guia explica o que são verificações de erro não markovianas e como implementá-las em software com qiskit-mitigation. Você começará com um pequeno exemplo de dois qubits para visualizar as verificações e discutir como elas funcionam. Depois de construir alguma intuição, você vai testar um exemplo de 20 qubits em que implementa verificações e compara várias estratégias de pós-seleção.
O que são verificações de erro não markovianas?
As verificações de erro não markovianas são testes baseados em medição que indicam se o estado de um qubit provavelmente foi corrompido por uma fonte de erro não markoviana. Esse teste é feito colocando uma porta de inversão de bit com duração estendida (xslow ou uma série de portas rx com ângulo ) em cada qubit no início de um circuito, seguida de uma XGate normal, e medindo para garantir que o estado foi invertido com sucesso de . Esse teste também pode ser feito no fim do circuito, após cada medição terminal, para garantir que o dispositivo consegue inverter com sucesso o estado medido do qubit de . Amostras associadas a verificações que falharam podem ser consideradas não confiáveis, e pós-selecionar apenas as amostras que passam em todas as verificações pode aumentar a fidelidade da distribuição ruidosa.
Estratégias para pós-seleção com base em verificações de erro não markovianas
-
Pós-seleção baseada em nós: pós-selecione apenas as amostras em que cada qubit passou em sua(s) verificação(ões). Essa é a estratégia mais rígida e geralmente resulta em mais amostras descartadas.
-
Pós-seleção baseada em arestas: para cada aresta no grafo de acoplamento dos qubits, garanta que pelo menos um qubit passe na(s) verificação(ões). Essa estratégia é mais branda; se um qubit falha em uma verificação, a amostra ainda pode ser mantida se todos os seus vizinhos passarem em suas verificações.
Qubits espectadores
É possível especificar qubits auxiliares "espectadores" adjacentes aos qubits usados para executar o circuito original. As verificações de erro não markovianas podem ser colocadas nesses espectadores, oferecendo mais cobertura contra o ruído não markoviano que essas verificações foram projetadas para detectar.
Parte 1. Visualize verificações de erro não markovianas
Antes de demonstrar a técnica em um QPU, você criará um pequeno circuito de dois qubits e adicionará verificações para visualizar como ela funciona. Comece com um estado de Bell transpilado para o QPU de 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")
Adicione algumas verificações de erro não markovianas ao início do circuito. As verificações só serão colocadas em qubits que terminam com uma medição. Essas verificações garantem que o dispositivo consegue inverter com sucesso o estado inicial do qubit de , e as medições dessas verificações são lidas em um novo registrador clássico chamado 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)
Da mesma forma, você pode adicionar verificações ao fim do circuito após cada medição terminal. Essas verificações garantem que o dispositivo consegue inverter o estado do qubit de , e suas medições são lidas em um novo registrador clássico chamado 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)
Como mencionado na introdução, você pode usar qubits ancilla adjacentes aos qubits de dados como "espectadores" para fornecer mais cobertura de erros. Adicione verificações de espectador pré-circuito e pós-circuito. Você pode ver que, como o qubit físico 2 em ibm_kingston é adjacente ao qubit de dados 1, ele foi adicionado como espectador, e verificações pré e pós-circuito foram adicionadas a ele. As medições dos espectadores foram adicionadas a novos registradores clássicos chamados spec_pre e spec.
Um circuito como este contém todas as verificações de erros não markovianos possíveis, então amostrar a partir dele dá aos usuários a capacidade de fazer pós-seleção com base em qualquer combinação de verificações [pré/pós]-circuito e estratégia de pós-seleção baseada em [nós/arestas]. Veja a Parte 2 deste guia para saber como isso funciona em hardware quântico.
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)

Parte 2: Demonstre verificações de erros não markovianos em uma QPU e compare estratégias de pós-seleção
Agora que você entende o que são as verificações de erros não markovianos e como elas são implementadas em software, você pode executar um exemplo maior em uma QPU e ver como ele funciona. Primeiro, prepare um circuito. A escolha do circuito não é muito importante; você só precisa de um circuito cuja saída seja fácil de verificar. Aqui escolhemos um circuito de Ising espelhado 1D de 20 qubits. Esse circuito não é trivial de executar em hardware e deve ser afetado por erros não markovianos, o que oferece a oportunidade de demonstrar a eficácia das verificações de erros não markovianos. Além disso, a estrutura espelhada nos permite comparar trivialmente os cálculos de valores esperados ruidosos com os valores exatos.
Neste experimento, estamos calculando valores esperados de observáveis. A escolha do observável também não é importante, então escolhemos simplesmente a magnetização média de um único sítio .
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
Agora adicionamos verificações pré e pós-circuito em todos os qubits, incluindo os sete qubits espectadores adjacentes. Quando você adiciona todas as verificações possíveis a um circuito, pode decidir qual estratégia de pós-seleção usar durante o pós-processamento.
# 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)]
Use a primitiva Executor para amostrar o circuito que contém as verificações.
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
A classe PostSelector conhece a estrutura de creg das verificações, então consegue associar cada bit das amostras à sua medição de circuito correspondente. Seu método compute_mask pode ser usado para criar uma máscara sobre uma sequência de amostras, dados a estratégia de pós-seleção (nó ou aresta) e o tipo de verificação de erros não markovianos a considerar (pré ou pós-circuito). Para combinar as estratégias pré e pós-circuito em uma única máscara, suas máscaras podem ser calculadas de forma independente e depois combinadas com a operação AND. Aqui usamos o PostSelector para criar máscaras para todas as várias estratégias e examinar os resultados usando todas as permutações de estratégias disponíveis.
Na visualização abaixo, vemos como a estratégia de pós-seleção afeta a taxa de pós-seleção, bem como a qualidade da resposta. Como esperado, as estratégias que envolvem pós-seleção baseada em nós resultaram nas menores taxas de pós-seleção. As estratégias baseadas em arestas são mais permissivas e tendem a resultar em taxas de pós-seleção mais altas. Os usuários devem experimentar as várias estratégias no seu problema para determinar o equilíbrio certo entre a taxa de pós-seleção e a qualidade da distribuição amostrada. Neste exemplo, usar pós-seleção baseada em nós com as verificações pré e pós-circuito resultou na melhor resposta, com uma taxa de pós-seleção de .028%. Algumas das estratégias não superaram o circuito bruto. Você também pode ver que adicionar as verificações sem usar nenhuma delas (barra rotulada "no selection" no gráfico abaixo) tem o pior desempenho.
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)

