Skip to main content
IBM Quantum Platform

Compare transpiler settings

Estimación de uso: menos de un minuto en un procesador Eagle r3 (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)


Fondo

Para garantizar resultados más rápidos y eficientes, a partir del 1 de marzo de 2024, los circuitos y observables deben transformarse para utilizar únicamente instrucciones compatibles con la QPU (unidad de procesamiento cuántico) antes de ser sometidos a las primitivas Qiskit Runtime. Los llamamos circuitos de arquitectura de conjunto de instrucciones (ISA) y observables. Una forma habitual de hacerlo es utilizar la función generate_preset_pass_manager del transpilador. Sin embargo, puede optar por seguir un proceso más manual.

Por ejemplo, es posible que desee dirigirse a un subconjunto específico de qubits en un dispositivo específico. Este tutorial prueba el rendimiento de diferentes configuraciones del transpilador completando todo el proceso de creación, transpilación y envío de circuitos.


Requisitos

Antes de empezar este tutorial, asegúrate de que tienes instalado lo siguiente:

  • Qiskit SDK v1.2 o posterior, con soporte de visualización
  • Qiskit Runtime v0.28 o posterior (pip install qiskit-ibm-runtime)

Setup

# 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,
)

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

Crea un pequeño circuito para que el transpilador intente optimizarlo. Este ejemplo crea un circuito que lleva a cabo el algoritmo de Grover con un oráculo que marca el estado 111. A continuación, simule la distribución ideal (lo que esperaría medir si ejecutara esto en un ordenador cuántico perfecto un número infinito de veces) para compararla más tarde.

# 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

Output:

'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:

Output of the previous code cell
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
 
plot_histogram(ideal_distribution)

Output:

Output of the previous code cell

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

A continuación, transpila los circuitos para la QPU. Comparará el rendimiento del transpilador con optimization_level ajustado a 0 (el más bajo) frente a 3 (el más alto). El nivel de optimización más bajo hace lo mínimo necesario para que el circuito funcione en el dispositivo: asigna los qubits del circuito a los qubits del dispositivo y añade puertas de intercambio para permitir todas las operaciones de dos qubits. El nivel de optimización más alto es mucho más inteligente y utiliza muchos trucos para reducir el número total de puertas. Dado que las puertas multiqubit tienen altas tasas de error y los qubits se descohesionan con el tiempo, los circuitos más cortos deberían dar mejores resultados.

La siguiente celda transpila qc para ambos valores de optimization_level, imprime el número de puertas de dos qubits, y añade los circuitos transpilados a una lista. Algunos de los algoritmos del transpilador son aleatorios, por lo que establece una semilla para la reproducibilidad.

# 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)

Output:

Two-qubit gates (optimization_level=0):  21
Two-qubit gates (optimization_level=3):  14

Dado que los CNOT suelen tener una alta tasa de error, el circuito transpilado con optimization_level=3 debería funcionar mucho mejor.

Otra forma de mejorar el rendimiento es mediante el desacoplamiento dinámico, aplicando una secuencia de puertas a los qubits en reposo. Esto anula algunas interacciones no deseadas con el entorno. La siguiente celda añade desacoplamiento dinámico al circuito transpilado con optimization_level=3 y lo añade a la 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:

Output of the previous code cell

Paso 3: Ejecutar usando primitivas Qiskit

En este punto, tienes una lista de circuitos transpilados para la QPU especificada. A continuación, cree una instancia de la primitiva sampler e inicie un trabajo por lotes utilizando el gestor de contexto (with ...:), que abre y cierra automáticamente el lote.

Dentro del gestor de contexto, muestrea los circuitos y almacena los resultados en result.

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

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

Por último, compare los resultados de las ejecuciones del dispositivo con la distribución ideal. Puedes ver que los resultados con optimization_level=3 se acercan más a la distribución ideal debido al menor número de puertas, y optimization_level=3 + dd se acerca aún más debido al desacoplamiento 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:

Output of the previous code cell

Puede confirmarlo calculando la fidelidad de Hellinger entre cada conjunto de resultados y la distribución ideal (mayor es mejor, y 1 es la fidelidad perfecta).

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

Output:

0.848
0.945
0.990

Encuesta tutorial

Responda a esta breve encuesta para darnos su opinión sobre este tutorial. Su opinión nos ayudará a mejorar nuestra oferta de contenidos y la experiencia de los usuarios.

Enlace a la encuesta

¿Le ha resultado útil esta página?
Informe de un error, errata o solicite contenidos en GitHub .