Pós-seleção com verificações de erros não markovianas
Este guia explica o que são verificações de erros 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 adquirir um pouco de intuição, você vai tentar um exemplo com 20 qubits, no qual implementará verificações e comparará várias estratégias de pós-seleção.
O que são verificações de erros não markovianas?
As verificações de erros não markovianos são testes baseados em medições que determinam se é provável que o estado de um qubit tenha sido corrompido por uma fonte de erro não markoviana. Esse teste é realizado colocando-se uma porta de inversão de bits com duração estendida (xslow ou uma série de portas rx com ângulo ) em cada qubit no início de um circuito, seguido por uma medição regular XGate para garantir que o estado tenha sido invertido com sucesso a partir de . Esse teste também pode ser realizado no final do circuito, após cada medição terminal, para garantir que o dispositivo consiga inverter com sucesso o estado medido do qubit a partir de . Amostras associadas a verificações com falha podem ser consideradas não confiáveis, e a pós-seleção apenas das amostras que passarem em todas as verificações pode aumentar a fidelidade da distribuição ruidosa.
Estratégias de pós-seleção baseadas em verificações de erros não markovianas
- Pós-seleção baseada em nós : selecione posteriormente apenas as amostras nas quais cada qubit tenha sido aprovado em suas verificações. Essa é a estratégia mais rigorosa e, geralmente, resulta na descarte de um número maior de amostras.
- Pós-seleção baseada em arestas : Para cada aresta no grafo de acoplamento dos qubits, certifique-se de que pelo menos um qubit seja aprovado na(s) verificação(ões). Essa estratégia é mais flexível; se um qubit não passar em uma verificação, a amostra ainda poderá ser mantida caso todos os seus vizinhos sejam aprovados nas respectivas verificações.
Qubits espectadores
É possível especificar qubits ancilla “espectadores” que sejam adjacentes aos qubits utilizados para executar o circuito original. É possível aplicar verificações de erros não markovianas a esses espectadores, proporcionando maior cobertura contra o ruído não markoviano que essas verificações foram projetadas para detectar.
Parte 1. Visualização de verificações de erros 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 ele funciona. Comece com um estado de Bell transpilado para o 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:
Adicione algumas verificações de erros não markovianas no início do circuito. As verificações serão realizadas apenas nos qubits que forem concluídos com uma medição. Essas verificações garantem que o dispositivo consiga inverter com sucesso o estado inicial do qubit de , e as medições dessas verificações são lidas em um novo registro 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)Output:
Da mesma forma, é possível adicionar verificações no final do circuito após cada medição nos terminais. Essas verificações garantem que o dispositivo possa inverter o estado do qubit de “ ”, e suas medições são lidas em um novo registro 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)Output:
Conforme mencionado na introdução, é possível utilizar qubits auxiliares adjacentes aos qubits de dados como “espectadores” para proporcionar maior cobertura contra erros. Incluir verificações com os espectadores antes e depois do circuito. É possível observar que, como o qubit físico 2 em é ibm_kingston adjacente ao qubit de dados 1, ele foi adicionado como espectador, e foram adicionadas verificações pré e pós-circuito a ele. As medidas do espectador foram adicionadas a novos registros clássicos chamados spec_pre e spec.
Um circuito como este contém todas as verificações de erro não markovianas possíveis; portanto, a amostragem a partir desse circuito permite que os usuários realizem a pós-seleção com base em qualquer combinação de verificações do [pre/post]circuito e de estratégias de pós-seleção [node/edge]baseadas nele. Consulte 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)Output:
Parte 2: Demonstrar verificações de erros não markovianas em uma QPU e comparar estratégias de pós-seleção
Agora que você já entende o que são verificações de erros não markovianas e como elas são implementadas em software, pode executar um exemplo mais extenso em uma QPU e ver como funciona.
Primeiro, prepare um circuito. A escolha do circuito não é muito importante; basta um circuito cuja saída você possa verificar facilmente. Aqui, escolhemos um circuito de Ising espelhado de 20 qubits, do tipo “ 1D ”. A execução desse circuito em hardware não é trivial e deve ser afetada 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 facilmente os cálculos do valor esperado com ruído com os valores exatos.
Nesta experiência, estamos calculando os valores esperados das variáveis observáveis. A escolha da observável também não é importante; portanto, simplesmente escolhemos a magnetização média por site .
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
Agora, adicionamos verificações pré e pós-circuito em cada qubit, incluindo os sete qubits espectadores adjacentes. Ao adicionar todas as verificações possíveis a um circuito, é possível decidir qual estratégia de pós-seleção utilizar 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])Output:
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()}")Output:
job id: d9badrm6hjac73ffe31g
status: QUEUED
A classe PostSelector conhece a creg estrutura das verificações, de modo que pode associar cada bit das amostras à medição do circuito correspondente. Seu compute_mask método pode ser utilizado para criar uma máscara sobre uma sequência de amostras, considerando a estratégia de pós-seleção (nó ou aresta) e o tipo de verificações de erros não markovianos a serem analisadas (pré ou pós-circuito). Para combinar estratégias pré e pós-circuito em uma única máscara, essas máscaras podem ser calculadas independentemente e, em seguida, combinadas por meio da operação AND . Aqui, utilizamos o PostSelector para criar máscaras para todas as diversas estratégias e analisamos os resultados utilizando 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 era de se esperar, as estratégias que envolveram a pós-seleção baseada em nós resultaram nas menores taxas de pós-seleção. As estratégias baseadas em bordas são mais flexíveis e tendem a resultar em taxas de pós-seleção mais altas. Os usuários devem testar as diversas estratégias em seu problema para determinar o equilíbrio adequado entre a taxa de pós-seleção e a qualidade da distribuição amostral. Neste exemplo, o uso da pós-seleção baseada em nós, juntamente 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 apresentaram desempenho superior ao do circuito bruto. Você também pode observar que adicionar as marcas de seleção, mas não usar nenhuma delas (a barra intitulada “sem seleção” no gráfico abaixo), apresenta 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()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)