Postselección con comprobaciones de errores no markovianas
En esta guía se explica qué son las comprobaciones de errores no markovianas y cómo implementarlas en software con qiskit-mitigation. Empezarás con un pequeño ejemplo de dos qubits para visualizar las comprobaciones y analizar cómo funcionan. Una vez que hayas adquirido cierta intuición, probarás un ejemplo de 20 qubits en el que implementarás comprobaciones y compararás diversas estrategias de postselección.
¿Qué son las comprobaciones de errores no markovianas?
Las comprobaciones de errores no markovianos son pruebas basadas en mediciones que permiten determinar si es probable que el estado de un qubit se haya visto alterado por una fuente de error no markoviana. Esta prueba se realiza colocando una puerta de inversión de bits de duración extendida (xslow o una serie de puertas rx con un ángulo ) en cada qubit al inicio de un circuito, seguida de una medición regular XGate para garantizar que el estado se ha invertido correctamente desde . Esta prueba también puede realizarse al final del circuito, tras cada medición terminal, para garantizar que el dispositivo puede invertir correctamente el estado medido del qubit desde . Las muestras asociadas a comprobaciones fallidas pueden considerarse poco fiables, y la poselección exclusiva de las muestras que superen todas las comprobaciones puede aumentar la fidelidad de la distribución ruidosa.
Estrategias de postselección basadas en comprobaciones de errores no markovianas
- Postselección basada en nodos : Selecciona posteriormente solo aquellas muestras en las que cada qubit haya superado sus comprobaciones. Esta es la estrategia más estricta y, por lo general, da lugar a que se descarten más muestras.
- Postselección basada en aristas : Para cada arista del grafo de acoplamiento de los qubits, hay que asegurarse de que al menos un qubit supere la(s) comprobación(es). Esta estrategia es más flexible; si un qubit no supera una comprobación, la muestra puede conservarse de todos modos si todos sus vecinos superan sus comprobaciones.
Qubits espectadores
Se pueden especificar qubits ancilla «espectadores» que sean adyacentes a los qubits utilizados para ejecutar el circuito original. Se pueden aplicar comprobaciones de errores no markovianos a estos espectadores, lo que proporciona una mayor protección frente al ruido no markoviano que estas comprobaciones están diseñadas para detectar.
Parte 1. Visualización de comprobaciones de errores no markovianas
Antes de demostrar la técnica en una QPU, crearás un pequeño circuito de dos qubits y añadirás comprobaciones para visualizar cómo funciona. Empieza con un estado de Bell transpilado al backend ibm_kingston de la 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:
Añade algunas comprobaciones de errores no markovianas al principio del circuito. Solo se aplicarán comprobaciones a los qubits que finalicen con una medición. Estas comprobaciones garantizan que el dispositivo pueda invertir correctamente el estado inicial del qubit de « », y las mediciones de estas comprobaciones se leen en un nuevo registro clásico denominado 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:
Del mismo modo, puedes añadir comprobaciones al final del circuito tras cada medición en los terminales. Estas comprobaciones garantizan que el dispositivo pueda invertir el estado del qubit de « », y sus mediciones se leen en un nuevo registro clásico denominado 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:
Como se ha mencionado en la introducción, se pueden utilizar qubits auxiliares adyacentes a los qubits de datos como «espectadores» para proporcionar una mayor cobertura frente a los errores. Incluir comprobaciones de los espectadores antes y después del circuito. Como puedes ver, dado que el qubit físico 2 en ibm_kingston es adyacente al qubit de datos 1, se ha añadido como espectador y se le han incorporado comprobaciones previas y posteriores al circuito. Las medidas de los espectadores se han añadido a los nuevos registros clásicos denominados spec_pre y spec.
Un circuito como este contiene todas las comprobaciones de error no markovianas posibles, por lo que el muestreo a partir de este circuito ofrece a los usuarios la posibilidad de realizar una postselección basada en cualquier combinación de comprobaciones del [pre/post]circuito y una estrategia de postselección [node/edge]basada en el mismo. Consulta la parte 2 de esta guía para ver cómo funciona en hardware cuá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: Demostrar comprobaciones de errores no markovianas en una QPU y comparar estrategias de postselección
Ahora que ya sabes en qué consisten las comprobaciones de errores no markovianas y cómo se implementan en el software, puedes ejecutar un ejemplo más amplio en una QPU y ver cómo funciona.
En primer lugar, prepara un circuito. La elección del circuito no es muy importante; solo necesitas un circuito cuya salida puedas comprobar fácilmente. En este caso, elegimos un circuito de Ising simétrico de 20 qubits, 1D. La ejecución de este circuito en hardware no es sencilla y debería verse afectada por errores no markovianos, lo que brinda la oportunidad de demostrar la eficacia de las comprobaciones de errores no markovianos. Además, la estructura simétrica nos permite comparar fácilmente los cálculos del valor esperado con ruido con los valores exactos.
En este experimento, estamos calculando los valores esperados de las magnitudes observables. La elección de la observable tampoco es importante, por lo que simplemente elegimos la magnetización media en un solo sitio .
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
Ahora añadimos comprobaciones previas y posteriores al circuito en cada qubit, incluidos los siete qubits «espectadores» adyacentes. Al añadir todas las comprobaciones posibles a un circuito, puedes decidir qué estrategia de poselección utilizar durante el posprocesamiento.
# 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)]
Utiliza la primitiva Executor para tomar muestras del circuito que contiene las comprobaciones.
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 clase PostSelector conoce la estructura creg de las comprobaciones, por lo que puede asociar cada bit de las muestras a la medición del circuito correspondiente. Su compute_mask método puede utilizarse para crear una máscara sobre una secuencia de muestras, dada la estrategia de poselección (nodo o arista) y el tipo de comprobaciones de errores no markovianos que se vayan a examinar (previas o posteriores al circuito). Para combinar estrategias previas y posteriores al circuito en una única máscara, estas se pueden calcular de forma independiente y, a continuación, combinarse mediante la operación AND . Aquí utilizamos el PostSelector para crear máscaras para todas las estrategias posibles y analizamos los resultados utilizando todas las permutaciones de estrategias disponibles.
En la visualización que aparece a continuación, vemos cómo la estrategia de postselección influye en la tasa de postselección, así como en la calidad de la respuesta. Como era de esperar, las estrategias que incluían la poselección basada en nodos dieron lugar a las tasas de poselección más bajas. Las estrategias basadas en los bordes son más flexibles y suelen dar lugar a tasas de postselección más elevadas. Los usuarios deberían probar las distintas estrategias con su problema para determinar el equilibrio adecuado entre la tasa de poselección y la calidad de la distribución muestreada. En este ejemplo, el uso de la poselección basada en nodos, junto con las comprobaciones previas y posteriores al circuito, dio como resultado la mejor respuesta, con una tasa de poselección de .028%. Algunas de las estrategias no obtuvieron mejores resultados que el circuito sin ajustar. También se puede observar que añadir las casillas de selección pero no utilizar ninguna de ellas (la barra etiquetada como «sin selección» en el gráfico siguiente) es la opción que da peores resultados.
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)