Pular para o conteúdo principal

Comparar configurações do transpilador

Estimativa de uso: menos de um minuto em um processador Eagle r3 (NOTA: Esta é apenas uma estimativa. O tempo de execução pode variar.)

Contexto

Para garantir resultados mais rápidos e eficientes, a partir de 1º de março de 2024, os circuitos e observáveis precisam ser transformados para usar apenas instruções suportadas pela QPU (unidade de processamento quântico) antes de serem enviados aos primitivos do Qiskit Runtime. Chamamos esses circuitos e observáveis de instruction set architecture (ISA). Uma forma comum de fazer isso é usar a função generate_preset_pass_manager do transpilador. No entanto, você pode optar por seguir um processo mais manual.

Por exemplo, você pode querer direcionar um subconjunto específico de qubits em um dispositivo específico. Este guia testa o desempenho de diferentes configurações do transpilador completando todo o processo de criação, transpilação e envio de circuitos.

Requisitos

Antes de começar, certifique-se de ter os seguintes pacotes instalados:

  • Qiskit SDK v1.2 ou posterior, com suporte a visualização
  • Qiskit Runtime v0.28 ou posterior (pip install qiskit-ibm-runtime)

Configuração

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-ibm-runtime
# Create circuit to test transpiler on
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.circuit.library import GroverOperator, Diagonal

# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.transpiler import PassManager

from qiskit.circuit.library import XGate
from qiskit.quantum_info import hellinger_fidelity

# Qiskit Runtime
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
from qiskit_ibm_runtime.transpiler.passes.scheduling import (
ASAPScheduleAnalysis,
PadDynamicalDecoupling,
)

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

Crie um pequeno circuito para o transpilador tentar otimizar. Este exemplo cria um circuito que executa o algoritmo de Grover com um oráculo que marca o estado 111. Em seguida, simule a distribuição ideal (o que você esperaria medir se executasse isso em um computador quântico perfeito um número infinito de vezes) para comparação posterior.

# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend.name
'ibm_brisbanse'
oracle = Diagonal([1] * 7 + [-1])
qc = QuantumCircuit(3)
qc.h([0, 1, 2])
qc = qc.compose(GroverOperator(oracle))

qc.draw(output="mpl", style="iqp")

Output of the previous code cell

ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()

plot_histogram(ideal_distribution)

Output of the previous code cell

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

Em seguida, transpile os circuitos para a QPU. Você vai comparar o desempenho do transpilador com optimization_level definido como 0 (mais baixo) contra 3 (mais alto). O nível de otimização mais baixo faz o mínimo necessário para que o circuito rode no dispositivo: mapeia os qubits do circuito para os qubits do dispositivo e adiciona portas swap para permitir todas as operações de dois qubits. O nível de otimização mais alto é muito mais inteligente e usa várias técnicas para reduzir a contagem total de portas. Como as portas de múltiplos qubits têm altas taxas de erro e os qubits decoerêm com o tempo, os circuitos mais curtos devem produzir melhores resultados.

A célula a seguir transpila qc para ambos os valores de optimization_level, imprime o número de portas de dois qubits e adiciona os circuitos transpilados a uma lista. Alguns algoritmos do transpilador são aleatorizados, portanto, é definida uma semente para reprodutibilidade.

# Need to add measurements to the circuit
qc.measure_all()

# Find the correct two-qubit gate
twoQ_gates = set(["ecr", "cz", "cx"])
for gate in backend.basis_gates:
if gate in twoQ_gates:
twoQ_gate = gate

circuits = []
for optimization_level in [0, 3]:
pm = generate_preset_pass_manager(
optimization_level, backend=backend, seed_transpiler=0
)
t_qc = pm.run(qc)
print(
f"Two-qubit gates (optimization_level={optimization_level}): ",
t_qc.count_ops()[twoQ_gate],
)
circuits.append(t_qc)
Two-qubit gates (optimization_level=0):  21
Two-qubit gates (optimization_level=3): 14

Como os CNOTs geralmente têm uma alta taxa de erro, o circuito transpilado com optimization_level=3 deve ter um desempenho muito melhor.

Outra forma de melhorar o desempenho é através do desacoplamento dinâmico (dynamic decoupling), aplicando uma sequência de portas a qubits ociosos. Isso cancela algumas interações indesejadas com o ambiente. A célula a seguir adiciona desacoplamento dinâmico ao circuito transpilado com optimization_level=3 e o adiciona à lista.

# Get gate durations so the transpiler knows how long each operation takes
durations = backend.target.durations()

# This is the sequence we'll apply to idling qubits
dd_sequence = [XGate(), XGate()]

# Run scheduling and dynamic decoupling passes on circuit
pm = PassManager(
[
ASAPScheduleAnalysis(durations),
PadDynamicalDecoupling(durations, dd_sequence),
]
)
circ_dd = pm.run(circuits[1])

# Add this new circuit to our list
circuits.append(circ_dd)
circ_dd.draw(output="mpl", style="iqp", idle_wires=False)

Output of the previous code cell

Passo 3: Executar usando os primitivos do Qiskit

Neste ponto, você tem uma lista de circuitos transpilados para a QPU especificada. Em seguida, crie uma instância do primitivo sampler e inicie um job em lote usando o gerenciador de contexto (with ...:), que abre e fecha o lote automaticamente.

Dentro do gerenciador de contexto, faça a amostragem dos circuitos e armazene os resultados em result.

with Batch(backend=backend):
sampler = Sampler()
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()

Passo 4: Pós-processar e retornar o resultado no formato clássico desejado

Por fim, plote os resultados das execuções no dispositivo em relação à distribuição ideal. Você pode ver que os resultados com optimization_level=3 estão mais próximos da distribuição ideal devido à menor contagem de portas, e optimization_level=3 + dd está ainda mais próximo devido ao desacoplamento dinâmico.

binary_prob = [
{
k: v / res.data.meas.num_shots
for k, v in res.data.meas.get_counts().items()
}
for res in result
]
plot_histogram(
binary_prob + [ideal_distribution],
bar_labels=False,
legend=[
"optimization_level=0",
"optimization_level=3",
"optimization_level=3 + dd",
"ideal distribution",
],
)

Output of the previous code cell

Você pode confirmar isso calculando a fidelidade de Hellinger entre cada conjunto de resultados e a distribuição ideal (quanto maior, melhor, sendo 1 a fidelidade perfeita).

for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")
0.848
0.945
0.990