Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

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 NN portas rx com ângulo πN\frac{\pi}{N}) 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 ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle. 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 ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle. 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")

Output of the previous code cell

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 ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle, 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)

Output of the previous code cell

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 ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle, 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)

Output of the previous code cell

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)

Output of the previous code cell

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 OO também não é importante, então escolhemos simplesmente a magnetização média de um único sítio 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

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)

Output of the previous code cell