Saltar al contenido principal

Entrelazamiento de largo alcance con circuitos dinámicos

Estimación de uso: 4 minutos en un procesador Heron r2. (NOTA: Esto es solo una estimación. Su tiempo de ejecución puede variar.)

Antecedentes

El entrelazamiento de largo alcance entre qubits distantes es un desafío en dispositivos con conectividad limitada. Este tutorial muestra cómo los circuitos dinámicos pueden generar dicho entrelazamiento implementando una compuerta controlled-X de largo alcance (LRCX) mediante un protocolo basado en mediciones.

Siguiendo el enfoque de Elisa Bäumer et al. en 1, el método utiliza mediciones a mitad de circuito y retroalimentación para lograr compuertas de profundidad constante independientemente de la separación entre qubits. Crea pares de Bell intermedios, mide un qubit de cada par y aplica compuertas condicionadas clásicamente para propagar el entrelazamiento a través del dispositivo. Esto evita largas cadenas de SWAP, reduciendo tanto la profundidad del circuito como la exposición a errores de compuertas de dos qubits.

En este cuaderno, adaptamos el protocolo para hardware de IBM Quantum® y lo extendemos para ejecutar múltiples operaciones LRCX en paralelo, lo que nos permite explorar cómo el rendimiento escala con el número de operaciones condicionales simultáneas.

Requisitos

Antes de comenzar este tutorial, asegúrate de tener instalado lo siguiente:

  • Qiskit SDK v2.0 o posterior, con soporte de visualization
  • Qiskit Runtime ( pip install qiskit-ibm-runtime ) v0.37 o posterior

Configuración

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.visualization import plot_circuit_layout
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
import matplotlib.pyplot as plt
import numpy as np

Paso 1: Mapear entradas clásicas a un problema cuántico

Ahora implementamos una compuerta CNOT de largo alcance entre dos qubits distantes, siguiendo la construcción de circuito dinámico que se muestra a continuación (adaptada de la Fig. 1a en Ref. 1). La idea clave es utilizar un "bus" de qubits ancilla, inicializados en 0|0\rangle, para mediar la teleportación de compuertas de largo alcance.

Long-range CNOT circuit

Como se ilustra en la figura, el proceso funciona de la siguiente manera:

  1. Preparar una cadena de pares de Bell que conecte los qubits de control y objetivo a través de ancillas intermedias.
  2. Realizar mediciones de Bell entre qubits vecinos no entrelazados, intercambiando el entrelazamiento paso a paso hasta que el control y el objetivo compartan un par de Bell.
  3. Utilizar este par de Bell para la teleportación de compuertas, convirtiendo un CNOT local en un CNOT de largo alcance determinista con profundidad constante.

Este enfoque reemplaza las largas cadenas de SWAP con un protocolo de profundidad constante, reduciendo la exposición a errores de compuertas de dos qubits y haciendo que la operación sea escalable con el tamaño del dispositivo.

A continuación, primero recorreremos la implementación del circuito LRCX con circuitos dinámicos. Al final, también proporcionaremos una implementación basada en operaciones unitarias para comparación, con el fin de destacar las ventajas de los circuitos dinámicos en este contexto.

(i) Inicializar circuito

Comenzamos con un problema cuántico simple que servirá como base para la comparación. Específicamente, inicializamos un circuito con un qubit de control en el índice 0 y le aplicamos una compuerta Hadamard. Esto produce un estado de superposición que, al ser seguido por una operación controlled-X, genera un estado de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2} entre los qubits de control y objetivo.

En esta etapa, aún no estamos construyendo el controlled-X de largo alcance (LRCX) en sí. En cambio, nuestro objetivo es definir un circuito inicial claro y mínimo que destaque el papel del LRCX. En el Paso 2, mostraremos cómo el LRCX puede implementarse como una optimización utilizando circuitos dinámicos, y compararemos su rendimiento con un equivalente unitario. Es importante señalar que el protocolo LRCX puede aplicarse a cualquier circuito inicial. Aquí utilizamos esta configuración simple con Hadamard para mayor claridad en la demostración.

distance = 6  # The distance of the CNOT gate, with the convention that a distance of zero is a nearest-neighbor CNOT.

def initialize_circuit(distance):
assert distance >= 0
control = 0 # control qubit
n = distance # number of qubits between target and control

qr = QuantumRegister(
n + 2, name="q"
) # Circuit with n qubits between control and target
cr = ClassicalRegister(
2, name="cr"
) # Classical register for measuring control and target qubits

k = int(n / 2) # Number of Bell States to be used

allcr = [cr]
if (
distance > 1
): # This classical register will be used to store ZZ measurements. It is only used for long-range CX gates with distance > 1
c1 = ClassicalRegister(
k, name="c1"
) # Classical register needed for post processing
allcr.append(c1)
if (
distance > 0
): # This classical register will be used to store XX measurements. It is only used if distance > 0
c2 = ClassicalRegister(
n - k, name="c2"
) # Classical register needed for post processing
allcr.append(c2)

qc = QuantumCircuit(qr, *allcr, name="CNOT")

# Apply a Hadamard gate to the control qubit such that the long-range CNOT gate will prepare a Bell state (|00> + |11>)/sqrt(2)
qc.h(control)

return qc

qc = initialize_circuit(distance)
qc.draw(fold=-1, output="mpl", scale=0.5)

Output of the previous code cell

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

En este paso, mostramos cómo construir el circuito LRCX utilizando circuitos dinámicos. El objetivo es optimizar el circuito para su ejecución en hardware reduciendo la profundidad en comparación con una implementación puramente unitaria. Para ilustrar los beneficios, mostraremos tanto la construcción dinámica del LRCX como su equivalente unitario, y luego compararemos su rendimiento después de la transpilación. Es importante señalar que, aunque aquí aplicamos el LRCX a un problema simple inicializado con Hadamard, el protocolo puede aplicarse a cualquier circuito donde se requiera un CNOT de largo alcance.

(ii) Preparar pares de Bell

Comenzamos creando una cadena de pares de Bell a lo largo del camino entre los qubits de control y objetivo. Si la distancia es impar, primero aplicamos un CNOT del control a su vecino, que es el CNOT que será teleportado. Para una distancia par, este CNOT se aplicarás después del paso de preparación de pares de Bell. La cadena de pares de Bell entonces entrelaza pares sucesivos de qubits, estableciendo el recurso necesario para transportar la información de control a través del dispositivo.

# Determine where to start the Bell pair chain and add an extra CNOT when n is odd
def check_even(n: int) -> int:
"""Return 1 if n is even, else 2."""
return 1 if n % 2 == 0 else 2

def prepare_bell_pairs(qc, add_barriers=True):
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)

if add_barriers:
qc.barrier()

x0 = check_even(n)
if n % 2 != 0:
qc.cx(0, 1)

# Create k Bell pairs
for i in range(k):
qc.h(x0 + 2 * i)
qc.cx(x0 + 2 * i, x0 + 2 * i + 1)
return qc

qc = prepare_bell_pairs(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)

Output of the previous code cell

(iii) Medir pares de qubits vecinos en la base de Bell

A continuación, medimos qubits vecinos no entrelazados en la base de Bell (mediciones de dos qubits de XXXX y ZZZZ). Esto crea un par de Bell de largo alcance entre el qubit objetivo y el qubit adyacente al control (salvo correcciones de Pauli, que se implementarán mediante retroalimentación en el siguiente paso). En paralelo, implementamos la medición entrelazante que teleporta la compuerta CNOT para que actúe sobre el qubit objetivo previsto.

def measure_bell_basis(qc, add_barriers=True):
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)

if n > 1:
_, c1, c2 = qc.cregs
elif n > 0:
_, c2 = qc.cregs

# Determine where to start the Bell pair chain and add an extra CNOT when n is odd
x0 = 1 if n % 2 == 0 else 2

# Entangling layer that implements the Bell measurement (and additionally adds the CNOT to be teleported, if n is even)
for i in range(k + 1):
qc.cx(x0 - 1 + 2 * i, x0 + 2 * i)

for i in range(1, k + x0):
if i == 1:
qc.h(2 * i + 1 - x0)
else:
qc.h(2 * i + 1 - x0)

if add_barriers:
qc.barrier()

# Map the ZZ measurements onto classical register c1
for i in range(k):
if i == 0:
qc.measure(2 * i + x0, c1[i])
else:
qc.measure(2 * i + x0, c1[i])

# Map the XX measurements onto classical register c2
for i in range(1, k + x0):
if i == 1:
qc.measure(2 * i + 1 - x0, c2[i - 1])
else:
qc.measure(2 * i + 1 - x0, c2[i - 1])
return qc

qc = measure_bell_basis(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)

Output of the previous code cell

(iv) A continuación, aplicar correcciones de retroalimentación para corregir los operadores de subproducto de Pauli

Las mediciones en la base de Bell introducen subproductos de Pauli que deben corregirse utilizando los resultados registrados. Esto se realiza en dos pasos. Primero, necesitamos calcular la paridad de todas las mediciones de ZZZZ, que luego se utiliza para aplicar condicionalmente una compuerta XX al qubit objetivo. De manera similar, se calcula la paridad de las mediciones de XXXX y se utiliza para aplicar condicionalmente una compuerta ZZ al qubit de control.

Con el nuevo marco de expresiones clásicas en Qiskit, estas paridades pueden calcularse directamente en la capa de procesamiento clásico del circuito. En lugar de aplicar una secuencia de compuertas condicionales individuales para cada bit de medición, podemos construir una única expresión clásica que represente el XOR (paridad) de todos los resultados de medición relevantes. Esta expresión se utiliza luego como la condición en un único bloque if_test, permitiendo que las compuertas de corrección se apliquen con profundidad constante. Este enfoque simplifica el circuito y garantiza que las correcciones de retroalimentación no introduzcan latencia adicional innecesaria.

def apply_ffwd_corrections(qc):
control = 0 # control qubit
target = qc.num_qubits - 1 # target qubit
n = qc.num_qubits - 2 # number of qubits between target and control

k = int(n / 2)
x0 = check_even(n)

if n > 1:
_, c1, c2 = qc.cregs
elif n > 0:
_, c2 = qc.cregs

# First, let's compute the parity of all ZZ measurements
for i in range(k):
if i == 0:
parity_ZZ = expr.lift(
c1[i]
) # Store the value of the first ZZ measurement in parity_ZZ
else:
parity_ZZ = expr.bit_xor(
c1[i], parity_ZZ
) # Successively compute the parity via XOR operations

for i in range(1, k + x0):
if i == 1:
parity_XX = expr.lift(
c2[i - 1]
) # Store the value of the first XX measurement in parity_XX
else:
parity_XX = expr.bit_xor(
c2[i - 1], parity_XX
) # Successively compute the parity via XOR operations

if n > 0:
with qc.if_test(parity_XX):
qc.z(control)

if n > 1:
with qc.if_test(parity_ZZ):
qc.x(target)
return qc

qc = apply_ffwd_corrections(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)

Output of the previous code cell

(v) Finalmente, medir los qubits de control y objetivo

Definimos una función auxiliar que permite la medición de los qubits de control y objetivo en las bases XXXX, YYYY o ZZZZ. Para verificar el estado de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2}, los valores esperados de XXXX y ZZZZ deben ser ambos +1+1, ya que son estabilizadores del estado. La medición de YYYY también se admite aquí y se utilizarás más adelante al calcular la fidelidad.

def measure_in_basis(qc, basis="XX", add_barrier=True):
control = 0 # control qubit
target = qc.num_qubits - 1 # target qubit

assert basis in ["XX", "YY", "ZZ"]

qc = (
qc.copy()
) # We copy the circuit because we want to measure in different bases
cr = qc.cregs[0]

if add_barrier:
qc.barrier()

if basis == "XX":
qc.h(control)
qc.h(target)
elif basis == "YY":
qc.sdg(control)
qc.sdg(target)
qc.h(control)
qc.h(target)

qc.measure(control, cr[0])
qc.measure(target, cr[1])
return qc

qc_YY = measure_in_basis(qc.copy(), basis="YY")
display(
qc_YY.draw(output="mpl", fold=-1, scale=0.5)
) # Circuit for measuring in the YY basis

Output of the previous code cell

Reunir todo

Combinamos los diversos pasos definidos anteriormente para crear una compuerta CX de largo alcance en los dos extremos de una línea 1D. Los pasos incluyen:

  • Inicializar el qubit de control en ket+\\ket{+}
  • Preparar pares de Bell
  • Medir pares de qubits vecinos
  • Aplicar correcciones de retroalimentación dependientes de las MCM
def lrcx(distance, prep_barrier=True, pre_measure_barrier=True):
qc = initialize_circuit(distance)
qc = prepare_bell_pairs(qc, prep_barrier)
qc = measure_bell_basis(qc, pre_measure_barrier)
qc = apply_ffwd_corrections(qc)
return qc

qc = lrcx(distance)
# Apply the measurement in the XX, YY, and ZZ bases
qc_XX, qc_YY, qc_ZZ = [
measure_in_basis(qc, basis=basis) for basis in ["XX", "YY", "ZZ"]
]

display(
qc_YY.draw(output="mpl", fold=-1, scale=0.5)
) # Circuit for measuring in the YY basis

Output of the previous code cell

Generar circuitos para diferentes distancias

Ahora generamos circuitos CX de largo alcance para un rango de separaciones entre qubits. Para cada distancia, construimos circuitos que miden en las bases XXXX, YYYY y ZZZZ, que se utilizarán posteriormente para calcular las fidelidades.

La lista de distancias incluye separaciones tanto de corto como de largo alcance, donde distance = 0 corresponde a un CX de vecinos más cercanos. Estas mismas distancias también se utilizarán para generar los circuitos unitarios correspondientes más adelante para la comparación.

distances = [
0,
1,
2,
3,
6,
11,
16,
21,
28,
35,
44,
55,
60,
] # Distances for long range CX. distance of 0 is a nearest-neighbor CX
distances.sort()
assert (
min(distances) >= 0
) # Only works for distance larger than 2 because classical register cannot be empty
basis_list = ["XX", "YY", "ZZ"]

circuits_dyn = []
for distance in distances:
for basis in basis_list:
circuits_dyn.append(
measure_in_basis(lrcx(distance, prep_barrier=False), basis=basis)
)
print(f"Number of circuits: {len(circuits_dyn)}")
circuits_dyn[14].draw(fold=-1, output="mpl", idle_wires=False)
Number of circuits: 39

Output of the previous code cell

Implementación basada en operaciones unitarias intercambiando los qubits hacia el centro

Para comparación, primero examinamos el caso en el que una compuerta CNOT de largo alcance se implementa utilizando conexiones de vecinos más cercanos y compuertas unitarias. En la siguiente figura, a la izquierda se muestra un circuito para una compuerta CNOT de largo alcance que abarca una cadena 1D de n-qubits sujeta únicamente a conexiones de vecinos más cercanos. En el centro se muestra una descomposición unitaria equivalente implementable con compuertas CNOT locales, con profundidad de circuito O(n)O(n).

Long-range CNOT circuit

El circuito del centro puede implementarse de la siguiente manera:

def cnot_unitary(distance):
"""Generate a long range CNOT gate using local CNOTs on a 1D chain of qubits subject to n
nearest-neighbor connections only.

Args:
distance (int) : The distance of the CNOT gate, with the convention that a distance of 0 is a nearest-neighbor CNOT.

Returns:
QuantumCircuit: A Quantum Circuit implementing a long-range CNOT gate between qubit 0 and qubit distance+1
"""
assert distance >= 0
n = distance # number of qubits between target and control

qr = QuantumRegister(
n + 2, name="q"
) # Circuit with n qubits between control and target
cr = ClassicalRegister(
2, name="cr"
) # Classical register for measuring control and target qubits

qc = QuantumCircuit(qr, cr, name="CNOT_unitary")

control_qubit = 0

qc.h(control_qubit) # Prepare the control qubit in the |+> state

k = int(n / 2)
qc.barrier()
for i in range(control_qubit, control_qubit + k):
qc.cx(i, i + 1)
qc.cx(i + 1, i)
qc.cx(-i - 1, -i - 2)
qc.cx(-i - 2, -i - 1)
if n % 2 == 1:
qc.cx(k + 2, k + 1)
qc.cx(k + 1, k + 2)
qc.barrier()
qc.cx(k, k + 1)
for i in range(control_qubit, control_qubit + k):
qc.cx(k - i, k - 1 - i)
qc.cx(k - 1 - i, k - i)
qc.cx(k + i + 1, k + i + 2)
qc.cx(k + i + 2, k + i + 1)
if n % 2 == 1:
qc.cx(-2, -1)
qc.cx(-1, -2)

return qc

Ahora construimos todos los circuitos unitarios, y construimos los circuitos que miden en las bases XXXX, YYYY y ZZZZ, tal como hicimos anteriormente para los circuitos dinámicos.

circuits_uni = []
for distance in distances:
for basis in basis_list:
circuits_uni.append(
measure_in_basis(cnot_unitary(distance), basis=basis)
)

print(f"Number of circuits: {len(circuits_uni)}")
circuits_uni[14].draw(fold=-1, output="mpl", idle_wires=False)
Number of circuits: 39

Output of the previous code cell

Ahora que tenemos tanto los circuitos dinámicos como los unitarios para un rango de distancias, estamos listos para la transpilación. Primero necesitamos seleccionar un dispositivo backend.

# Set up access to IBM Quantum devices
from qiskit.circuit import IfElseOp

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=156
)

El siguiente paso asegura que el backend admita la instrucción if_else, que es necesaria para la versión más reciente de los circuitos dinámicos. Dado que esta funcionalidad aún está en acceso anticipado, añadimos explícitamente la operación IfElseOp al target del backend si aún no está disponible.

if "if_else" not in backend.target.operation_names:
backend.target.add_instruction(IfElseOp, name="if_else")

Usar la cadena de Layer Fidelity para seleccionar una cadena 1D

Dado que queremos comparar el rendimiento de los circuitos dinámicos y unitarios en una cadena 1D, utilizamos la cadena de Layer Fidelity para seleccionar una topología lineal de la mejor cadena de qubits del dispositivo. Esto asegura que ambos tipos de circuitos se transpilen bajo las mismas restricciones de conectividad, permitiendo una comparación justa de su rendimiento.

# This selects best qubits for longest distance and uses the same control for all lengths
lf_qubits = backend.properties().to_dict()[
"general_qlists"
] # best linear chain qubits
chosen_layouts = {
distance: [
val["qubits"]
for val in lf_qubits
if val["name"] == f"lf_{distances[-1] + 2}"
][0][: distance + 2]
for distance in distances
}
print(chosen_layouts[max(distances)]) # best qubits at each distance
[10, 11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 58, 51, 50, 49, 48, 47, 46, 45, 44, 43, 56, 63, 62, 61, 76, 81, 82, 83, 84, 85, 77, 65, 66, 67, 68, 69, 78, 89, 90, 91, 98, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101]
isa_circuits_dyn = []
isa_circuits_uni = []

# Using the same initial layouts for both circuits for better apples to apples comparison
for qc in circuits_dyn:
pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=chosen_layouts[qc.num_qubits - 2],
)
isa_circuits_dyn.append(pm.run(qc))

for qc in circuits_uni:
pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=chosen_layouts[qc.num_qubits - 2],
)
isa_circuits_uni.append(pm.run(qc))
print(
f"2Q depth: {isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_dyn[14].draw("mpl", fold=-1, idle_wires=0)
2Q depth: 2

Output of the previous code cell

print(
f"2Q depth: {isa_circuits_uni[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_uni[14].draw("mpl", fold=-1, idle_wires=False)
2Q depth: 13

Output of the previous code cell

Visualizar los qubits utilizados para el circuito LRCX

En esta sección, examinamos cómo el circuito LRCX se mapea en el hardware. Comenzamos visualizando los qubits físicos utilizados en el circuito y luego estudiamos cómo la distancia control-objetivo en el diseño afecta el número de operaciones.

# Note: the qubit coordinates must be hard-coded.
# The backend API does not currently provide this information directly.
# If using a different backend, you will need to adjust the coordinates accordingly,
# or set the qubit_coordinates = None to use the default layout coordinates.

def _heron_coords_r2():
"""Generate coordinates for the Heron layout in R2. Note"""
cord_map = np.array(
[
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
],
-1
* np.array([j for i in range(15) for j in [i] * [16, 4][i % 2]]),
],
dtype=int,
)

hcords = []
ycords = cord_map[0]
xcords = cord_map[1]
for i in range(156):
hcords.append([xcords[i] + 1, np.abs(ycords[i]) + 1])

return hcords

# Visualize the active qubits in the circuit layout
plot_circuit_layout(
circuit=isa_circuits_uni[-1],
backend=backend,
view="physical",
qubit_coordinates=_heron_coords_r2(),
)

Output of the previous code cell

Paso 3: Ejecutar utilizando primitivas de Qiskit

En este paso, ejecutamos el experimento en el backend especificado. También hacemos uso de la agrupación por lotes para ejecutar eficientemente el experimento a lo largo de múltiples ensayos. Ejecutar ensayos repetidos nos permite calcular promedios para una comparación más precisa entre los métodos unitario y dinámico, así como cuantificar su variabilidad comparando las desviaciones entre ejecuciones.

print(backend.name)
ibm_kingston

Selecciona el número de ensayos y realiza la ejecución por lotes.

num_trials = 10
jobs_uni = []
jobs_dyn = []
with Batch(backend=backend) as batch:
sampler = Sampler(mode=batch)
for _ in range(num_trials):
jobs_uni.append(sampler.run(isa_circuits_uni, shots=1024))
jobs_dyn.append(sampler.run(isa_circuits_dyn, shots=1024))

Paso 4: Post-procesar y devolver el resultado en el formato clásico deseado

Después de que los experimentos se han ejecutado exitosamente, ahora post-procesamos los conteos de mediciones para extraer métricas significativas. En este paso:

  • Definimos métricas de calidad para evaluar el rendimiento del CX de largo alcance.
  • Calculamos valores esperados de operadores de Pauli a partir de los resultados de medición sin procesar.
  • Utilizamos estos para calcular la fidelidad del estado de Bell generado.

Este análisis proporciona una imagen clara de qué tan bien se desempeñan los circuitos dinámicos en relación con la implementación de referencia unitaria.

Métricas de calidad

Para evaluar el éxito del protocolo CX de largo alcance, medimos qué tan cerca está el estado de salida del estado de Bell ideal. Una forma conveniente de cuantificar esto es calculando la fidelidad del estado utilizando valores esperados de operadores de Pauli. La fidelidad para un estado de Bell en el estado de control y objetivo puede calcularse después de conocer XX\braket{XX}, YY\braket{YY} y ZZ\braket{ZZ}. En particular,

F=14(1+XXYY+ZZ) F = \frac{1}{4} (1 + \braket{XX} - \braket{YY} + \braket{ZZ})

Para calcular estos valores esperados a partir de datos de medición sin procesar, definimos un conjunto de funciones auxiliares:

  • compute_ZZ_expectation: Dados los conteos de medición, calcula el valor esperado de un operador de Pauli de dos qubits en la base ZZ.
  • compute_fidelity: Combina los valores esperados de XXXX, YYYY y ZZZZ en la expresión de fidelidad anterior.
  • get_counts_from_bitarray: Utilidad para extraer conteos de los objetos de resultado del backend.
def compute_ZZ_expectation(counts):
total = sum(counts.values())
expectation = 0
for bitstring, count in counts.items():
# Ensure bitstring is 2 bits
z1 = (-1) ** (int(bitstring[-1]))
z2 = (-1) ** (int(bitstring[-2]))
expectation += z1 * z2 * count
return expectation / total

def compute_fidelity(counts_xx, counts_yy, counts_zz):
xx, yy, zz = [
compute_ZZ_expectation(c) for c in [counts_xx, counts_yy, counts_zz]
]
return 1 / 4 * (1 + xx - yy + zz)

Calculamos la fidelidad para los circuitos CX dinámicos de largo alcance. Para cada distancia, extraemos los resultados de medición en las bases XX\braket{XX}, YY\braket{YY} y ZZ\braket{ZZ}. Estos resultados se combinan utilizando las funciones auxiliares definidas previamente para calcular la fidelidad según F=14(1+XXYY+ZZ)F = \tfrac{1}{4} \big( 1 + \langle XX \rangle - \langle YY \rangle + \langle ZZ \rangle \big). Esto proporciona la fidelidad observada del protocolo ejecutado dinámicamente a cada distancia.

fidelities_dyn = []

# loop over trials
for job in jobs_dyn:
result_dyn = job.result()
trial_fidelities = []
# loop over all distances
for ind, dist in enumerate(distances):
counts_xx = result_dyn[ind * 3].data.cr.get_counts()
counts_yy = result_dyn[ind * 3 + 1].data.cr.get_counts()
counts_zz = result_dyn[ind * 3 + 2].data.cr.get_counts()
trial_fidelities.append(
compute_fidelity(counts_xx, counts_yy, counts_zz)
)
fidelities_dyn.append(trial_fidelities)
# average over trials for each distance
avg_fidelities_dyn = np.mean(fidelities_dyn, axis=0)
std_fidelities_dyn = np.std(fidelities_dyn, axis=0)

Ahora calculamos la fidelidad para los circuitos CX unitarios de largo alcance, y lo hacemos de la misma manera que para los circuitos dinámicos anteriores.

fidelities_uni = []

# loop over trials
for job in jobs_uni:
result_uni = job.result()
trial_fidelities = []
# loop over all distances
for ind, dist in enumerate(distances):
counts_xx = result_uni[ind * 3].data.cr.get_counts()
counts_yy = result_uni[ind * 3 + 1].data.cr.get_counts()
counts_zz = result_uni[ind * 3 + 2].data.cr.get_counts()
trial_fidelities.append(
compute_fidelity(counts_xx, counts_yy, counts_zz)
)
fidelities_uni.append(trial_fidelities)
# average over trials for each distance
avg_fidelities_uni = np.mean(fidelities_uni, axis=0)
std_fidelities_uni = np.std(fidelities_uni, axis=0)

Graficar los resultados

Para apreciar los resultados visualmente, la siguiente celda grafica las fidelidades estimadas de la compuerta medidas a distancias variables entre qubits entrelazados para los métodos.

fig, ax = plt.subplots()

# Unitary with error bars
ax.errorbar(
distances,
avg_fidelities_uni,
yerr=std_fidelities_uni,
fmt="o-.",
color="c",
ecolor="c",
elinewidth=1,
capsize=4,
label="Unitary",
)
# Dynamic with error bars
ax.errorbar(
distances,
avg_fidelities_dyn,
yerr=std_fidelities_dyn,
fmt="o-.",
color="m",
ecolor="m",
elinewidth=1,
capsize=4,
label="Dynamic",
)
# Random gate baseline
ax.axhline(y=1 / 4, linestyle="--", color="gray", label="Random gate")

legend = ax.legend(frameon=True)
for text in legend.get_texts():
text.set_color("black")
legend.get_frame().set_facecolor("white")
legend.get_frame().set_edgecolor("black")
ax.set_title(
"Bell State Fidelity vs Control–Target Separation", color="black"
)
ax.set_xlabel("Distance", color="black")
ax.set_ylabel("Bell state fidelity", color="black")
ax.grid(linestyle=":", linewidth=0.6, alpha=0.4, color="gray")
ax.set_ylim((0.2, 1))
ax.set_facecolor("white")
fig.patch.set_facecolor("white")
for spine in ax.spines.values():
spine.set_visible(True)
spine.set_color("black")
ax.tick_params(axis="x", colors="black")
ax.tick_params(axis="y", colors="black")
plt.show()

Output of the previous code cell

A partir del gráfico de fidelidad anterior, el LRCX no superó consistentemente la implementación unitaria directa. De hecho, para separaciones cortas entre control y objetivo, el circuito unitario logró mayor fidelidad. Sin embargo, a mayores separaciones, el circuito dinámico comienza a alcanzar mejor fidelidad que la implementación unitaria. Este comportamiento no es inesperado en el hardware actual: aunque los circuitos dinámicos reducen la profundidad del circuito al evitar largas cadenas de SWAP, introducen tiempo de circuito adicional por las mediciones a mitad de circuito, la retroalimentación clásica y los retardos en la ruta de control. La latencia añadida aumenta la decoherencia y los errores de lectura, lo que puede superar los ahorros de profundidad a distancias cortas.

No obstante, observamos un punto de cruce donde el enfoque dinámico supera al unitario. Este es un resultado directo del escalamiento diferente: la profundidad del circuito unitario crece linealmente con la distancia entre qubits, mientras que la profundidad del circuito dinámico permanece constante.

Puntos clave:

  • Beneficio inmediato de los circuitos dinámicos: La principal motivación actual es la reducción de la profundidad de dos qubits, no necesariamente una mejora en la fidelidad.
  • Por qué la fidelidad puede ser peor actualmente: El aumento del tiempo de circuito por las operaciones de medición y clásicas a menudo domina, especialmente cuando la separación control-objetivo es pequeña.
  • De cara al futuro: A medida que el hardware mejore -- específicamente con lecturas más rápidas, menor latencia en el control clásico y menor sobrecarga de medición a mitad de circuito -- deberíamos esperar que estas reducciones de profundidad y duración se traduzcan en ganancias de fidelidad medibles.
# Compute metrics for each distance, skipping the basis circuits since they are identical for each distance
depths_2q_dyn = [
c.depth(lambda x: x.operation.num_qubits == 2)
for c in isa_circuits_dyn[::3]
]
meas_dyn = [
sum(1 for instr in c.data if instr.operation.name == "measure")
for c in isa_circuits_dyn[::3]
]

depths_2q_uni = [
c.depth(lambda x: x.operation.num_qubits == 2)
for c in isa_circuits_uni[::3]
]
meas_uni = [
sum(1 for instr in c.data if instr.operation.name == "measure")
for c in isa_circuits_uni[::3]
]

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].plot(
distances, depths_2q_uni, "o-.", color="c", label="Unitary (2Q depth)"
)
axes[0].plot(
distances, depths_2q_dyn, "o-.", color="m", label="Dynamic (2Q depth)"
)
axes[0].set_xlabel("Number of qubits between control and target")
axes[0].set_ylabel("Two-qubit depth")
axes[0].grid(True, linestyle=":", linewidth=0.6, alpha=0.4)
axes[0].legend()

axes[1].plot(
distances, meas_uni, "o-.", color="c", label="Unitary (# measurements)"
)
axes[1].plot(
distances, meas_dyn, "o-.", color="m", label="Dynamic (# measurements)"
)
axes[1].set_xlabel("Number of qubits between control and target")
axes[1].set_ylabel("Number of measurements")
axes[1].grid(True, linestyle=":", linewidth=0.6, alpha=0.4)
axes[1].legend()

fig.suptitle("Scaling of Unitary vs Dynamic LRCX with Distance", fontsize=12)

plt.tight_layout()
plt.show()

Output of the previous code cell

Este gráfico de profundidad de dos qubits destaca la ventaja principal del LRCX implementado con circuitos dinámicos: el rendimiento permanece esencialmente constante a medida que aumenta la separación entre los qubits de control y objetivo. En contraste, la implementación unitaria crece linealmente con la distancia debido a las cadenas de SWAP requeridas. La profundidad captura el escalamiento lógico de las operaciones de dos qubits, mientras que el conteo de mediciones refleja la sobrecarga adicional para los circuitos dinámicos. Estas mediciones son eficientes, ya que se realizan en paralelo, pero aún introducen un costo fijo en el hardware actual.

Por qué la fidelidad puede ser peor actualmente: El aumento del tiempo de circuito por las operaciones de medición y clásicas a menudo domina, especialmente cuando la separación control-objetivo es pequeña. Por ejemplo, la duración promedio de lectura en un procesador Heron r2 es de 2,280 ns, mientras que la duración de su compuerta de 2Q es de solo 68 ns.

A medida que las latencias de medición y clásicas mejoren, esperamos que el escalamiento de profundidad constante y mediciones constantes de los circuitos dinámicos produzca ventajas claras en fidelidad y tiempo de ejecución en circuitos más grandes.

Referencias

[1] Efficient Long-Range Entanglement using Dynamic Circuits, by Elisa Bäumer, Vinay Tripathi, Derek S. Wang, Patrick Rall, Edward H. Chen, Swarnadeep Majumder, Alireza Seif, Zlatko K. Minev. IBM Quantum, (2023). https://arxiv.org/abs/2308.13065