Skip to main content
IBM Quantum Platform

Entrelacamento de longo alcance com circuitos dinâmicos

Estimativa de uso: 4 minutos em um processador Heron r2. (OBSERVAÇÃO: essa é apenas uma estimativa. Seu tempo de execução pode variar)


Resultados do aprendizado

Ao concluir este tutorial, você terá aprendido o seguinte:

  • Como implementar uma porta CNOT de longo alcance utilizando circuitos dinâmicos com medições no meio do circuito (MCMs) e alimentação direta clássica;
  • Como implementar a porta equivalente utilizando uma abordagem baseada em SWAP unitário;
  • Como comparar ambas as abordagens medindo a fidelidade da porta em função da distância entre os qubits.

Pré-requisitos

Recomendamos que os usuários estejam familiarizados com os seguintes tópicos antes de seguir com este tutorial:


Segundo plano

O emaranhamento de longo alcance entre qubits distantes é um desafio em dispositivos com conectividade limitada. Este tutorial mostra como os circuitos dinâmicos podem gerar esse emaranhamento implementando uma porta LRCX (long-range controlled-X) usando um protocolo baseado em medição.

Seguindo a abordagem de Elisa Bäumer et al. em 1, o método usa a medição do meio do circuito e o feedforward para obter 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 de forma clássica para propagar o emaranhamento pelo dispositivo. Isso evita longas cadeias SWAP, reduzindo a profundidade do circuito e a exposição a erros de porta de dois qubits.

Neste artigo, adaptamos o protocolo para um hardwar IBM Quantum e e avaliamos seu desempenho em função da distância entre o controlador e o alvo, comparando-o com uma linha de base unitaria baseada no SWAP.


Requisitos

Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:

  • Qiskit SDK v2.0 ou posterior, com suporte para visualização
  • Qiskit Runtime v0.37 ou posterior (pip install qiskit-ibm-runtime)
  • Qiskit Aer v0.17 ou versão posterior (pip install qiskit-aer)

Instalação

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

Exemplo de simulador em pequena escala

Antes de executar o código na QPU real, verificamos se tanto os circuitos dinâmicos quanto os unitários produzem um estado de Bell ideal em um simulador sem ruído. Utilizamos o Qiskit RuntimeSampler com AerSimulator o modo backend, a uma distância de apenas 6.

Passo 1: Mapear entradas clássicas para um problema quântico

Implementamos agora 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 central é utilizar um “barramento” de qubits auxiliares, inicializados em 0|0\rangle, para mediar o teletransporte por portas de longo alcance.

Circuito CNOT de longo alcance

Conforme ilustrado na figura, o processo funciona da seguinte forma:

  1. Prepare uma cadeia de pares de Bell conectando os qubits de controle e de destino por meio de ancillas intermediárias.
  2. Realize 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.
  3. Use esse par de Bell para teletransporte de porta, transformando um CNOT local em um CNOT determinístico de longo alcance em profundidade constante.

Essa abordagem substitui as longas cadeias SWAP por um protocolo de profundidade constante, reduzindo a exposição a erros de porta de dois qubits e tornando a operação escalonável com o tamanho do dispositivo.

A seguir, vamos primeiro analisar a implementação do circuito dinâmico do circuito LRCX. No final, também forneceremos uma implementação baseada em unidade para comparação, a fim de destacar as vantagens dos circuitos dinâmicos nesse cenário.

Inicializar circuito

Começamos com um problema quântico simples que servirá de base para a comparação. Mais especificamente, inicializamos um circuito com um qubit de controle no índice 0 e aplicamos uma porta de Hadamard a ele. Isso produz um estado de superposição que, quando seguido por uma operação X controlada, gera um estado de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2} entre os qubits de controle e os qubits-alvo.

Nesta fase, ainda não estamos construindo o próprio LRCX (long-range controlled-X). Em vez disso, nosso objetivo é definir um circuito inicial claro e mínimo que destaque a função da LRCX. Na Etapa 2, mostraremos como o LRCX pode ser implementado como uma otimização usando circuitos dinâmicos e compararemos seu desempenho com um equivalente unitário. É importante ressaltar que o protocolo LRCX pode ser aplicado a qualquer circuito inicial. Aqui, usamos essa configuração simples de Hadamard para facilitar a 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)

Output:

Output of the previous code cell

Etapa 2: Otimizar o problema para execução em hardware quântico

Nesta etapa, mostramos como construir o circuito LRCX usando circuitos dinâmicos. O objetivo é otimizar o circuito para execução no hardware, reduzindo a profundidade em comparação com uma implementação puramente unitária. Para ilustrar os benefícios, exibiremos a construção dinâmica do LRCX e 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 por Hadamard, o protocolo pode ser aplicado a qualquer circuito em que seja necessário um CNOT de longo alcance.

Preparar pares de sinos

Começamos criando uma cadeia de pares de Bell ao longo do caminho entre os qubits de controle e de destino. Se a distância for ímpar, primeiro aplicamos um CNOT do controle ao seu vizinho, que é o CNOT que será teletransportado. Para uma distância uniforme, esse CNOT será aplicado após a etapa de preparação do par de sinos. Em seguida, a cadeia de pares Bell entrelaça pares sucessivos de qubits, estabelecendo o recurso necessário para transportar as informações de controle pelo 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:

Output of the previous code cell

Medir pares de qubits vizinhos na base de Bell

Em seguida, medimos os qubits vizinhos não emaranhados na base de Bell (medições de dois qubits de XXXX e ZZZZ ). Isso cria um par de Bell de longo alcance entre o qubit de destino e o qubit adjacente ao controle (até as correções de Pauli, que serão implementadas via feedforward na próxima etapa). Em paralelo, implementamos a medição de emaranhamento que teletransporta a porta CNOT para atuar no qubit de destino 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)

Output:

Output of the previous code cell

Aplicar correções antecipadas para corrigir os operadores de subprodutos de Pauli

As medições da base de Bell introduzem subprodutos de Pauli que devem ser corrigidos usando os resultados registrados. Isso é feito em duas etapas. Primeiro, precisamos computar a paridade de todas as medições do ZZZZ, que é então usada para aplicar condicionalmente uma porta XX ao qubit de destino. Da mesma forma, a paridade das medições do XXXX é calculada e usada para aplicar condicionalmente uma porta ZZ ao qubit de controle.

Com a nova estrutura de expressão clássica do 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 criar uma única expressão clássica que represente o XOR (paridade) de todos os resultados de medição relevantes. Essa expressão é então usada como a condição em um único bloco if_test , permitindo que as portas de correção sejam aplicadas em profundidade constante. Essa 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)

Output:

Output of the previous code cell

Controle de medição e qubits-alvo

Definimos uma função auxiliar que permite a medição dos qubits de controle e de destino nas bases XXXX, YYYY ou ZZZZ. Para verificar o estado de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2}, os valores de expectativa de XXXX e ZZZZ devem ser ambos +1+1, pois são estabilizadores do estado. A medição YYYY 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")
qc_YY.draw(
    output="mpl", fold=-1, scale=0.5
)  # Circuit for measuring in the YY basis

Output:

Output of the previous code cell

Junte tudo

Combinamos as várias etapas definidas acima para criar um portão CX de longo alcance nas duas extremidades de uma linha unidimensional ( 1D ). As etapas são as seguintes:

  • Inicializando o qubit de controle em +|+\rangle
  • Preparação dos pares de sinos
  • Medição de pares de qubits vizinhos
  • Aplicação de correções de feedforward dependentes dos 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"]
]

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

Output:

Output of the previous code cell

Implementação baseada em unitário, trocando os qubits para o meio

Para fins de comparação, examinamos primeiro o caso em que uma porta CNOT de longo alcance é implementada utilizando conexões entre vizinhos mais próximos e portas unitárias. Na figura a seguir, à esquerda, encontra-se um circuito para uma porta CNOT de longo alcance que abrange uma cadeia de n qubits do tipo “ 1D ”, sujeita apenas a conexões entre vizinhos mais próximos. No meio está uma decomposição unitária equivalente que pode ser implementada com portas CNOT locais; a profundidade do circuito é de O(n)O(n).

Circuito CNOT de longo alcance

O circuito do 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


qc_uni = cnot_unitary(distance)

Agora monte os circuitos que medem nas bases de XXXX, YYYY e ZZZZ, exatamente como fizemos com os circuitos dinâmicos acima.

# Apply the measurement in the XX, YY, and ZZ bases
qc_uni_XX, qc_uni_YY, qc_uni_ZZ = [
    measure_in_basis(qc_uni, basis=basis) for basis in ["XX", "YY", "ZZ"]
]

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

Output:

Output of the previous code cell

Agora que criamos tanto os circuitos dinâmicos quanto os circuitos unitários para um exemplo em pequena escala com distance=6, procedemos à sua transpilagem para executá-los inicialmente em um simulador sem ruído.

from qiskit_aer import AerSimulator

aer_backend = AerSimulator()
pm_sim = generate_preset_pass_manager(
    optimization_level=0, backend=aer_backend
)

# Dynamic circuits
isa_sim_dyn = pm_sim.run([qc_XX, qc_YY, qc_ZZ])

# Unitary circuits
isa_sim_uni = pm_sim.run([qc_uni_XX, qc_uni_YY, qc_uni_ZZ])

Passo 3: Execute usando Qiskit primitives

Agora podemos executar o experimento no backend do simulador sem ruído. Utilizamos o Sampler do Qiskit Runtime com o modo de backend AerSimulator para executar os circuitos.

sampler_sim = Sampler(mode=aer_backend)
sim_job = sampler_sim.run(isa_sim_dyn + isa_sim_uni)
sim_results = sim_job.result()

Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado

Após a execução bem-sucedida dos experimentos, procedemos agora ao pós-processamento dos dados de medição para extrair métricas significativas. Nesta etapa, fazemos o seguinte:

  • Definir métricas de qualidade para avaliar o desempenho da CX de longo prazo.
  • Calcule os valores de expectativa dos operadores Pauli a partir de resultados de medição brutos.
  • Use-os para calcular a fidelidade do estado de Bell gerado.

Em uma simulação sem ruído, verificaremos se a métrica de fidelidade é 11 para os circuitos construídos. Nas experiências realizadas com QPUs reais, essa análise fornecerá uma visão clara do desempenho dos circuitos dinâmicos em comparação com a implementação unitarária de referência.

Métricas de qualidade

Para avaliar o sucesso do protocolo CX de longo alcance, medimos o grau de proximidade entre o estado de saída e o estado de Bell ideal. Uma maneira prática de quantificar isso é calcular a fidelidade do estado utilizando os valores esperados dos operadores de Pauli. Podemos calcular a fidelidade de um estado de Bell no estado de controle e no estado alvo após conhecer XX\braket{XX}, YY\braket{YY} e ZZ\braket{ZZ}. Em particular,

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

Para calcular esses valores de expectativa a partir de dados de medição brutos, definimos um conjunto de funções auxiliares:

  • compute_ZZ_expectation: Com base nas contagens de medição, calcula o valor de expectativa de um operador Pauli de dois qubits na base ZZ.
  • compute_fidelity: Combina os valores de expectativa de XXXX, YYYY e ZZZZ na expressão de fidelidade acima.
  • get_counts_from_bitarray: Utilitário para extrair contagens de objetos de resultados de 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)
# Dynamic fidelity
counts_xx = sim_results[0].data.cr.get_counts()
counts_yy = sim_results[1].data.cr.get_counts()
counts_zz = sim_results[2].data.cr.get_counts()
fidelity_dyn = compute_fidelity(counts_xx, counts_yy, counts_zz)

# Unitary fidelity
counts_xx = sim_results[3].data.cr.get_counts()
counts_yy = sim_results[4].data.cr.get_counts()
counts_zz = sim_results[5].data.cr.get_counts()
fidelity_uni = compute_fidelity(counts_xx, counts_yy, counts_zz)

print(f"Dynamic fidelity (distance={distance}): {fidelity_dyn:.4f}")
print(f"Unitary fidelity (distance={distance}): {fidelity_uni:.4f}")

Output:

Dynamic fidelity (distance=6): 1.0000
Unitary fidelity (distance=6): 1.0000

Como era de se esperar em uma simulação sem ruído, as fidelidades tanto nos circuitos dinâmicos quanto nos circuitos unitários são 11.


Exemplo de hardware em grande escala

Aqui, reunimos todos esses detalhes em um único fluxo de trabalho em escala maior, que é então executado em hardware quântico real.

Gerar circuitos para diferentes distâncias

Atualmente, geramos circuitos CX de longo alcance para uma variedade de distâncias entre qubits de até 60 qubits. Para cada distância, construímos circuitos que medem nas bases XXXX, YYYY e ZZZZ, que serão posteriormente utilizados para calcular as fidelidades.

A lista de distâncias inclui separações de curto e longo alcance, com distance = 0 correspondendo a um CX de vizinho mais próximo. Essas mesmas distâncias também serão usadas para gerar os circuitos unitários correspondentes posteriormente para comparação.

# -------------------------Step 1-------------------------
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
basis_list = ["XX", "YY", "ZZ"]

# Dynamic circuits
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)}")

# Unitary circuits
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)}")

Agora que temos circuitos dinâmicos e unitários para uma série de distâncias, estamos prontos para a transpilação. Primeiro, precisamos selecionar um dispositivo de backend.

# -------------------------Step 2-------------------------
# 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 seja compatível com a instrução if_else , necessária para a versão mais recente dos circuitos dinâmicos. Como esse recurso ainda está em acesso antecipado, adicionamos explicitamente o IfElseOp ao destino de 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")

Use a string Layer Fidelity para selecionar uma cadei 1D

Como queremos comparar o desempenho de circuitos dinâmicos e unitários em uma cadeia 1D, usamos a cadeia 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

Output:

[11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 76, 81, 82, 83, 84, 85, 86, 87, 97, 107, 108, 109, 110, 111, 98, 91, 92, 93, 94, 95, 99, 115, 114, 113, 119, 133, 132, 131, 138, 151, 150, 149, 148]
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: "
    f"{isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_dyn[14].draw("mpl", fold=-1, idle_wires=0)

Output:

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

Output:

2Q depth: 13
Output of the previous code cell

Visualize os 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, em seguida, estudamos como a distância entre o controle e o alvo no layout afeta 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(),
)

Output:

Output of the previous code cell

Em seguida, executamos o experimento no backend real. Também utilizamos o processamento em lote para executar o experimento de forma eficiente em várias tentativas. A realização de ensaios repetidos permite calcular médias para uma comparação mais precisa entre os métodos unitário e dinâmico, bem como quantificar sua variabilidade por meio da comparação dos desvios entre as execuções.

# -------------------------Step 3-------------------------
num_trials = 10
jobs_uni = []
jobs_dyn = []
with Batch(backend=backend) as batch:
    sampler = Sampler(mode=batch)
    sampler.options.environment.job_tags = ["TUT_LRE"]
    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))

Calculamos a fidelidade dos circuitos CX dinâmicos de longo alcance. Para cada distância, extraímos os resultados das medições nas bases de dados XX\braket{XX}, YY\braket{YY} e ZZ\braket{ZZ}. Esses resultados são combinados utilizando as funções auxiliares definidas anteriormente para calcular a fidelidade de acordo com F=14(1+XXYY+ZZ)F = \tfrac{1}{4} \big( 1 + \langle XX \rangle - \langle YY \rangle + \langle ZZ \rangle \big). Isso fornece a fidelidade observada do protocolo executado dinamicamente em cada distância.

# -------------------------Step 4-------------------------
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 dos circuitos CX unitários de longo alcance e fazemos isso da mesma forma que fizemos com 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)

Plote os resultados

Para apreciar os resultados visualmente, o gráfico abaixo apresenta as fidelidades estimadas das portas, medidas em diferentes distâncias entre qubits entrelaçados para cada método.

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:

Output of the previous code cell

Com base no gráfico de fidelidade acima, o LRCX não superou de forma consistente a implementação unitária direta. Na verdade, para separações curtas entre controle e alvo, o circuito unitário obteve 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. Esse comportamento não é inesperado no hardware atual: embora os circuitos dinâmicos reduzam a profundidade do circuito, evitando longas cadeias de SWAP, eles introduzem um tempo de circuito adicional a partir de medições no meio do circuito, feedforward clássico e atrasos no caminho de controle. A latência adicional aumenta a decoerência e os erros de leitura, que podem superar a economia de profundidade em distâncias curtas.

No entanto, observamos um ponto de cruzamento em que a abordagem dinâmica supera a unitária. Esse é um resultado direto do dimensionamento 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 importantes:

  • Benefício imediato dos circuitos dinâmicos: A principal motivação atual é a redução da profundidade de dois qubits, não necessariamente o aumento da fidelidade.
  • Por que a fidelidade pode ser pior atualmente: O aumento do tempo de circuito das operações clássicas e de medição geralmente domina, especialmente quando a separação entre o controle e o alvo é pequena.
  • Olhando para o futuro: À medida que o hardware melhora, especificamente a leitura mais rápida, a latência de controle clássico mais curta e a redução da sobrecarga no meio do circuito, devemos esperar que essas 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()

Output:

Output of the previous code cell

Esse 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 de destino aumenta. Em contrapartida, a implementação unitária cresce linearmente com a distância devido às cadeias SWAP necessárias. A profundidade captura o dimensionamento lógico das operações de dois qubits, enquanto a contagem de medições reflete a sobrecarga adicional dos circuitos dinâmicos. Essas medições são eficientes, pois são realizadas em paralelo, mas ainda assim introduzem um custo fixo no hardware atual.

Por que a fidelidade pode ser pior atualmente: O aumento do tempo de circuito das operações clássicas e de medição geralmente domina, especialmente quando a separação entre o controle e o alvo é pequena. Por exemplo, o comprimento médio de leitura em um processador Heron r2 é de 2.280 ns, enquanto o comprimento da porta 2Q é de apenas 68 ns.

À medida que as latências clássicas e de medição melhoram, esperamos que o dimensionamento de profundidade constante e de medição constante dos circuitos dinâmicos produza fidelidade clara e vantagens de tempo de execução em circuitos maiores.


Próximas etapas

Se você achou este trabalho interessante, talvez se interesse pelos seguintes materiais:


Referências

[1] Efficient Long-Range Entanglement using Dynamic Circuits, por 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

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.