Emaranhamento de longo alcance com circuitos dinâmicos
Estimativa de uso: 4 minutos em um processador Heron r2. (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)
Contexto
O emaranhamento de longo alcance entre qubits distantes é desafiador em dispositivos com conectividade limitada. Este tutorial mostra como circuitos dinâmicos podem gerar tal emaranhamento implementando uma porta controlled-X de longo alcance (LRCX) usando um protocolo baseado em medição.
Seguindo a abordagem de Elisa Bäumer et al. em 1, o método usa medição no meio do circuito e feedforward para alcançar portas de profundidade constante independentemente da separação dos qubits. Ele cria pares de Bell intermediários, mede um qubit de cada par e aplica portas condicionadas classicamente para propagar o emaranhamento através do dispositivo. Isso evita longas cadeias de SWAP, reduzindo tanto a profundidade do circuito quanto a exposição a erros de portas de dois qubits.
Neste notebook, adaptamos o protocolo para hardware IBM Quantum® e o estendemos para executar múltiplas operações LRCX em paralelo, permitindo explorar como o desempenho escala com o número de operações condicionais simultâneas.
Requisitos
Antes de começar este tutorial, certifique-se de ter o seguinte instalado:
- Qiskit SDK v2.0 ou posterior, com suporte para visualização
- Qiskit Runtime (
pip install qiskit-ibm-runtime) v0.37 ou posterior
Configuração
# 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
Passo 1: Mapear entradas clássicas para um problema quântico
Agora implementamos uma porta CNOT de longo alcance entre dois qubits distantes, seguindo a construção de circuito dinâmico mostrada abaixo (adaptada da Fig. 1a na Ref. 1). A ideia chave é usar um "barramento" de qubits ancila, inicializados em , para mediar a teletransportação de porta de longo alcance.

Como ilustrado na figura, o processo funciona da seguinte forma:
- Preparar uma cadeia de pares de Bell conectando os qubits de controle e alvo via ancilas intermediárias.
- Realizar medições de Bell entre qubits vizinhos não-emaranhados, trocando o emaranhamento passo a passo até que o controle e o alvo compartilhem um par de Bell.
- Usar este par de Bell para teletransportação de porta, transformando um CNOT local em um CNOT de longo alcance determinístico de profundidade constante.
Esta abordagem substitui longas cadeias de SWAP por um protocolo de profundidade constante, reduzindo a exposição a erros de portas de dois qubits e tornando a operação escalável com o tamanho do dispositivo.
No que segue, primeiro percorreremos a implementação de circuito dinâmico do circuito LRCX. No final, também forneceremos uma implementação baseada em unitário para comparação, a fim de destacar as vantagens dos circuitos dinâmicos neste cenário.
(i) Inicializar circuito
Começamos com um problema quântico simples que servirá como base para comparação. Especificamente, inicializamos um circuito com um qubit de controle no índice 0 e aplicamos uma porta Hadamard a ele. Isso produz um estado de superposição que, quando seguido por uma operação controlled-X, gera um estado de Bell entre os qubits de controle e alvo.
Neste estágio, ainda não estamos construindo o controlled-X de longo alcance (LRCX) em si. Em vez disso, nosso objetivo é definir um circuito inicial claro e mínimo que destaque o papel do LRCX. No Passo 2, mostraremos como o LRCX pode ser implementado como uma otimização usando circuitos dinâmicos, e compararemos seu desempenho contra um equivalente unitário. Importante ressaltar que o protocolo LRCX pode ser aplicado a qualquer circuito inicial. Aqui usamos esta configuração simples de Hadamard para clareza de demonstração.
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)
Passo 2: Otimizar problema para execução em hardware quântico
Neste passo, mostramos como construir o circuito LRCX usando circuitos dinâmicos. O objetivo é otimizar o circuito para execução em hardware reduzindo a profundidade em comparação com uma implementação puramente unitária. Para ilustrar os benefícios, exibiremos tanto a construção LRCX dinâmica quanto seu equivalente unitário, e posteriormente compararemos seu desempenho após a transpilação. Importante ressaltar que, embora aqui apliquemos o LRCX a um problema simples inicializado com Hadamard, o protocolo pode ser aplicado a qualquer circuito onde um CNOT de longo alcance seja necessário.
(ii) Preparar pares de Bell
Começamos criando uma cadeia de pares de Bell ao longo do caminho entre os qubits de controle e alvo. Se a distância for ímpar, primeiro aplicamos um CNOT do controle para seu vizinho, que é o CNOT que será teletransportado. Para uma distância par, este CNOT será aplicado após a etapa de preparação dos pares de Bell. A cadeia de pares de Bell então emaranha pares sucessivos de qubits, estabelecendo o recurso necessário para transportar a informação de controle através do 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)
(iii) Medir pares de qubits vizinhos na base de Bell
Em seguida, medimos qubits vizinhos não-emaranhados na base de Bell (medições de dois qubits de e ). Isso cria um par de Bell de longo alcance entre o qubit alvo e o qubit adjacente ao controle (até correções de Pauli, que serão implementadas via feedforward no próximo passo). Em paralelo, implementamos a medição emaranhadora que teletransporta a porta CNOT para agir sobre o qubit alvo pretendido.
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)
(iv) Em seguida, aplicar correções de feedforward para corrigir operadores de subproduto de Pauli
As medições na base de Bell introduzem subprodutos de Pauli que devem ser corrigidos usando os resultados registrados. Isso é feito em duas etapas. Primeiro, precisamos calcular a paridade de todas as medições , que é então usada para aplicar condicionalmente uma porta ao qubit alvo. Da mesma forma, a paridade das medições é calculada e usada para aplicar condicionalmente uma porta ao qubit de controle.
Com o novo framework de expressão clássica no Qiskit, essas paridades podem ser calculadas diretamente na camada de processamento clássico do circuito. Em vez de aplicar uma sequência de portas condicionais individuais para cada bit de medição, podemos construir uma única expressão clássica que representa o XOR (paridade) de todos os resultados de medição relevantes. Esta expressão é então usada como condição em um único bloco if_test, permitindo que as portas de correção sejam aplicadas em profundidade constante. Esta abordagem simplifica o circuito e garante que as correções de feedforward não introduzam latência adicional desnecessária.
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)
(v) Finalmente, medir qubits de controle e alvo
Definimos uma função auxiliar que permite a medição dos qubits de controle e alvo nas bases , , ou . Para verificar o estado de Bell , os valores esperados de e devem ambos ser , pois são estabilizadores do estado. A medição também é suportada aqui e será usada abaixo ao calcular a fidelidade.
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
Juntando tudo
Combinamos as várias etapas definidas acima para criar uma porta CX de longo alcance em duas extremidades de uma linha 1D. As etapas incluem
- Inicializar o qubit de controle em
- Preparar pares de Bell
- Medir pares de qubits vizinhos
- Aplicar correções de feedforward dependentes das MCMs
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
Gerar circuitos para diferentes distâncias
Agora geramos circuitos CX de longo alcance para uma faixa de separações de qubits. Para cada distância, construímos circuitos que medem nas bases , e , que serão usados posteriormente para calcular fidelidades.
A lista de distâncias inclui separações tanto de curto quanto de longo alcance, com distance = 0 correspondendo a um CX entre vizinhos mais próximos. Essas mesmas distâncias também serão usadas para gerar os circuitos unitários correspondentes posteriormente para comparação.
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
Implementação baseada em unitários trocando os qubits para o meio
Para comparação, primeiro examinamos o caso em que uma porta CNOT de longo alcance é implementada usando conexões entre vizinhos mais próximos e portas unitárias. Na figura a seguir, à esquerda está um circuito para uma porta CNOT de longo alcance abrangendo uma cadeia 1D de n-qubits sujeita apenas a conexões entre vizinhos mais próximos. No meio está uma decomposição unitária equivalente implementável com portas CNOT locais, profundidade de circuito .

O circuito no meio pode ser implementado da seguinte forma:
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
Agora construímos todos os circuitos unitários e construímos os circuitos que medem nas bases , e , exatamente como fizemos para os circuitos dinâmicos acima.
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
Agora que temos circuitos dinâmicos e unitários para uma faixa de distâncias, estamos prontos para a transpilação. Primeiro precisamos selecionar um 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
)
A etapa a seguir garante que o backend suporte a instrução if_else, que é necessária para a versão mais recente de circuitos dinâmicos. Como esse recurso ainda está em acesso antecipado, adicionamos explicitamente o IfElseOp ao alvo do backend se ele ainda n ão estiver disponível.
if "if_else" not in backend.target.operation_names:
backend.target.add_instruction(IfElseOp, name="if_else")
Usar string de Layer Fidelity para selecionar cadeia 1D
Como queremos comparar o desempenho de circuitos dinâmicos e unitários em uma cadeia 1D, usamos a string de Layer Fidelity para selecionar uma topologia linear da melhor cadeia de qubits do dispositivo. Isso garante que ambos os tipos de circuitos sejam transpilados sob as mesmas restrições de conectividade, permitindo uma comparação justa de seu desempenho.
# 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

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

Visualizar qubits usados para o circuito LRCX
Nesta seção, examinamos como o circuito LRCX é mapeado no hardware. Começamos visualizando os qubits físicos usados no circuito e então estudamos como a distância controle-alvo no layout impacta o número de operações.
# 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(),
)

Passo 3: Executar usando primitivos do Qiskit
Neste passo, executamos o experimento no backend especificado. Também fazemos uso de batching para executar eficientemente o experimento através de múltiplas tentativas. Executar tentativas repetidas nos permite calcular médias para uma comparação mais precisa entre os métodos unitário e dinâmico, assim como quantificar sua variabilidade comparando os desvios entre as execuções.
print(backend.name)
ibm_kingston
Selecione o número de tentativas e execute em batch.
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))
Passo 4: Pós-processar e retornar resultado no formato clássico desejado
Após os experimentos terem sido executados com sucesso, agora pós-processamos as contagens de medição para extrair métricas significativas. Neste passo, nós:
- Definimos métricas de qualidade para avaliar o desempenho do CX de longo alcance.
- Calculamos valores de expectativa de operadores de Pauli a partir dos resultados brutos de medição.
- Usamos estes para calcular a fidelidade do estado de Bell gerado.
Esta análise fornece uma imagem clara de quão bem os circuitos dinâmicos performam em relação à implementação baseline unitária.
Métricas de qualidade
Para avaliar o sucesso do protocolo de CX de longo alcance, medimos quão próximo o estado de saída está do estado de Bell ideal. Uma maneira conveniente de quantificar isto é calculando a fidelidade do estado usando valores de expectativa de operadores de Pauli. A fidelidade para um estado de Bell nos qubits de controle e alvo pode ser calculada após conhecer , e . Em particular,
Para calcular estes valores de expectativa a partir de dados brutos de medição, definimos um conjunto de funções auxiliares:
compute_ZZ_expectation: Dadas as contagens de medição, calcula o valor de expectativa de um operador de Pauli de dois qubits na base .compute_fidelity: Combina os valores de expectativa de , e na expressão de fidelidade acima.get_counts_from_bitarray: Utilitário para extrair contagens dos objetos de resultado do 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 a fidelidade para os circuitos de CX dinâmico de longo alcance. Para cada distância, extraímos os resultados de medição nas bases , e . Estes resultados são combinados usando as funções auxiliares previamente definidas para calcular a fidelidade de acordo com . Isto fornece a fidelidade observada do protocolo executado dinamicamente em cada distância.
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)
Agora calculamos a fidelidade para os circuitos de CX unitário de longo alcance, e fazemos da mesma forma que fizemos para os circuitos dinâmicos acima.
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)
Plotar os resultados
Para apreciar os resultados visualmente, a célula abaixo plota as fidelidades de porta estimadas medidas em distâncias variadas entre qubits emaranhados para os 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()

A partir do gráfico de fidelidade acima, o LRCX não superou consistentemente a implementação unitária direta. De fato, para separações controle–alvo curtas, o circuito unitário alcançou maior fidelidade. No entanto, em separações maiores, o circuito dinâmico começa a alcançar melhor fidelidade do que a implementação unitária. Este comportamento não é inesperado no hardware atual: enquanto os circuitos dinâmicos reduzem a profundidade do circuito evitando longas cadeias de SWAP, eles introduzem tempo adicional de circuito devido a medições de meio-circuito, feedforward clássico e atrasos no caminho de controle. A latência adicional aumenta a decoerência e erros de leitura, o que pode superar as economias de profundidade em distâncias curtas.
No entanto, observamos um ponto de cruzamento onde a abordagem dinâmica supera a unitária. Isto é um resultado direto do escalonamento diferente: a profundidade do circuito unitário cresce linearmente com a distância entre os qubits, enquanto a profundidade do circuito dinâmico permanece constante.
Pontos-chave:
- Benefício imediato dos circuitos dinâmicos: A principal motivação atual é a profundidade de dois qubits reduzida, não necessariamente fidelidade melhorada.
- Por que a fidelidade pode ser pior hoje: O tempo de circuito aumentado devido a medições e operações clássicas frequentemente domina, especialmente quando a separação controle–alvo é pequena.
- Olhando para o futuro: À medida que o hardware melhora, especificamente leitura mais rápida, latência de controle clássico mais curta e overhead de meio-circuito reduzido, devemos esperar que estas reduções de profundidade e duração se traduzam em ganhos mensuráveis de fidelidade.
# 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()

Este gráfico de profundidade de dois qubits destaca a principal vantagem do LRCX implementado com circuitos dinâmicos: o desempenho permanece essencialmente constante à medida que a separação entre os qubits de controle e alvo aumenta. Em contraste, a implementação unitária cresce linearmente com a distância devido às cadeias de SWAP necessárias. A profundidade captura o escalonamento lógico de operações de dois qubits, enquanto a contagem de medições reflete o overhead adicional para circuitos dinâmicos. Estas medições são eficientes, já que são realizadas em paralelo, mas ainda introduzem um custo fixo no hardware atual.
Por que a fidelidade pode ser pior hoje: O tempo de circuito aumentado devido a medições e operações clássicas frequentemente domina, especialmente quando a separação controle-alvo é pequena. Por exemplo, o comprimento médio de leitura em um processador Heron r2 é de 2.280 ns, enquanto seu comprimento de porta 2Q é de apenas 68 ns.
À medida que as latências de medição e clássicas melhoram, esperamos que o escalonamento de profundidade constante e medição constante dos circuitos dinâmicos produza claras vantagens de fidelidade e tempo de execução em circuitos maiores.
Referências
[1] Efficient Long-Range Entanglement using Dynamic Circuits, by