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.nameOutput:
'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:
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
plot_histogram(ideal_distribution)Output:
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:
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:
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.