Comparer les paramètres du transpileur
Estimation d'utilisation : moins d'une minute sur un processeur Eagle r3 (REMARQUE : il s'agit d'une estimation uniquement. Ton temps d'exécution peut varier.)
Contexte
Pour garantir des résultats plus rapides et plus efficaces, depuis le 1er mars 2024, les circuits et les observables doivent être transformés pour n'utiliser que les instructions prises en charge par le QPU (unité de traitement quantique) avant d'être soumis aux primitives Qiskit Runtime. On appelle ces circuits et observables des circuits et observables à architecture de jeu d'instructions (ISA). Une façon courante de procéder est d'utiliser la fonction generate_preset_pass_manager du transpileur. Cependant, tu pourrais choisir de suivre un processus plus manuel.
Par exemple, tu pourrais vouloir cibler un sous-ensemble spécifique de qubits sur un appareil particulier. Cette démonstration teste les performances de différents paramètres du transpileur en effectuant le processus complet de création, de transpilation et de soumission de circuits.
Prérequis
Avant de commencer, assure-toi d'avoir installé les éléments suivants :
- Qiskit SDK v1.2 ou ultérieur, avec le support de la visualisation
- Qiskit Runtime v0.28 ou ultérieur (
pip install qiskit-ibm-runtime)
Configuration
# 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,
)
Étape 1 : Mapper les entrées classiques vers un problème quantique
Crée un petit circuit que le transpileur va essayer d'optimiser. Cet exemple crée un circuit qui exécute l'algorithme de Grover avec un oracle qui marque l'état 111. Ensuite, simule la distribution idéale (ce que tu t'attendrais à mesurer si tu exécutais ce circuit sur un ordinateur quantique parfait un nombre infini de fois) pour la comparer plus tard.
# 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")
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
plot_histogram(ideal_distribution)
Étape 2 : Optimiser le problème pour l'exécution sur matériel quantique
Ensuite, transpile les circuits pour le QPU. Tu compareras les performances du transpileur avec optimization_level réglé à 0 (le plus bas) par rapport à 3 (le plus haut). Le niveau d'optimisation le plus bas fait le strict minimum nécessaire pour faire fonctionner le circuit sur l'appareil ; il mappe les qubits du circuit vers les qubits de l'appareil et ajoute des portes swap pour permettre toutes les opérations à deux qubits. Le niveau d'optimisation le plus élevé est bien plus intelligent et utilise de nombreuses astuces pour réduire le nombre total de portes. Comme les portes multi-qubits ont des taux d'erreur élevés et que les qubits se décohèrent au fil du temps, les circuits plus courts devraient donner de meilleurs résultats.
La cellule suivante transpile qc pour les deux valeurs de optimization_level, affiche le nombre de portes à deux qubits, et ajoute les circuits transpilés à une liste. Certains algorithmes du transpileur étant aléatoires, elle définit une graine pour la reproductibilité.
# 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
Comme les CNOT ont généralement un taux d'erreur élevé, le circuit transpilé avec optimization_level=3 devrait offrir de bien meilleures performances.
Une autre façon d'améliorer les performances est le découplage dynamique, en appliquant une séquence de portes aux qubits inactifs. Cela annule certaines interactions indésirables avec l'environnement. La cellule suivante ajoute le découplage dynamique au circuit transpilé avec optimization_level=3 et l'ajoute à la liste.
# 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)

Étape 3 : Exécuter avec les primitives Qiskit
À ce stade, tu disposes d'une liste de circuits transpilés pour le QPU spécifié. Ensuite, crée une instance de la primitive sampler et démarre un job par lot en utilisant le gestionnaire de contexte (with ...:), qui ouvre et ferme automatiquement le lot.
Dans le gestionnaire de contexte, échantillonne les circuits et stocke les résultats dans result.
with Batch(backend=backend):
sampler = Sampler()
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()
Étape 4 : Post-traiter et retourner le résultat au format classique souhaité
Enfin, trace les résultats des exécutions sur l'appareil par rapport à la distribution idéale. Tu peux constater que les résultats avec optimization_level=3 sont plus proches de la distribution idéale en raison du nombre de portes réduit, et que optimization_level=3 + dd est encore plus proche grâce au découplage dynamique.
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",
],
)
Tu peux confirmer cela en calculant la fidélité de Hellinger entre chaque ensemble de résultats et la distribution idéale (plus la valeur est élevée, mieux c'est, et 1 correspond à une fidélité parfaite).
for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")
0.848
0.945
0.990