Qiskit en profondeur
Maîtriser le SDK quantique d'IBM — construction de circuits avec QuantumCircuit, portes personnalisées avec Gate/UnitaryGate, méthodes .inverse() et .control(), gestion des qubits (QuantumRegister, AncillaRegister), et l'écosystème Qiskit (qiskit, qiskit-aer, qiskit-ibm-runtime) — puis transpilation, primitives Sampler/Estimator et runtime IBM. Sans prérequis mathématiques.
Qiskit — circuits et portes
Qiskit est le SDK quantique d’IBM, écrit en Python. Tu construis des QuantumCircuit abstraits avec des portes idéales, puis tu les transpiles et les exécutes sur un simulateur ou du vrai matériel. Cette première section plonge dans la construction de circuits, les portes personnalisées et la gestion des qubits — la boîte à outils de base avant de parler transpilation et runtime.
Construire un QuantumCircuit — la brique de base
Un QuantumCircuit est l’objet central de Qiskit. Tu le crées en spécifiant des registres quantiques et classiques, puis tu y ajoutes des portes une par une. Le circuit est mutable : chaque appel comme qc.h(0) ou qc.cx(0, 1) ajoute une porte à la suite.
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
# Registres nommés — utile pour les circuits complexes
qr = QuantumRegister(3, name="data")
cr = ClassicalRegister(3, name="mesure")
qc = QuantumCircuit(qr, cr)
qc.h(qr[0])
qc.cx(qr[0], qr[1])
qc.cx(qr[0], qr[2]) # état GHZ à 3 qubits
qc.measure(qr, cr)
print(qc.draw())
Point clé :
QuantumCircuitest un objet mutable auquel on ajoute des portes séquentiellement. C’est un style impératif — tu décris le circuit instruction par instruction, comme un programme classique. Les registres nommés (QuantumRegister,ClassicalRegister) permettent de structurer les circuits complexes en groupes logiques de qubits.
Portes personnalisées avec Gate et UnitaryGate
Qiskit fournit toutes les portes standard (H, X, CX, T, etc.), mais tu peux aussi créer les tiennes. Deux approches :
UnitaryGate: tu fournis directement la matrice unitaire de ta porte.- Sous-circuit converti en porte : tu construis un petit
QuantumCircuit, puis tu appelles.to_gate()pour le transformer en porte réutilisable.
from qiskit import QuantumCircuit
from qiskit.circuit.library import UnitaryGate
import numpy as np
# Créer une porte personnalisée à partir d'une matrice unitaire
# Exemple : une "porte de phase personnalisée" appliquant e^(iπ/5) sur |1⟩
phi = np.pi / 5
matrice = np.array([[1, 0],
[0, np.exp(1j * phi)]])
porte_custom = UnitaryGate(matrice, label="P(π/5)")
qc = QuantumCircuit(1)
qc.append(porte_custom, [0])
print(qc.draw())
# On peut aussi créer une porte à partir d'un sous-circuit
sub = QuantumCircuit(2, name="bell")
sub.h(0)
sub.cx(0, 1)
porte_bell = sub.to_gate() # convertit le sous-circuit en porte réutilisable
Analogie : créer une porte personnalisée, c’est comme définir une fonction réutilisable en programmation.
UnitaryGate= tu écris le comportement brut (la matrice)..to_gate()= tu encapsules un bloc de code existant dans une fonction nommée que tu peux appeler partout.
.inverse() et .control() — transformer des portes existantes
En quantique, toute porte est réversible — son inverse existe toujours. Et toute porte peut être rendue conditionnelle via un qubit de contrôle. Qiskit rend ces deux transformations triviales :
.inverse()renvoie l’inverse (l’adjoint/dagger) d’une porte ou d’un circuit entier. L’ordre des portes est inversé et chaque porte est remplacée par son adjoint. C’est le « Ctrl+Z » quantique..control(n)renvoie une version contrôlée parnqubits. La porte ne s’applique que si tous les qubits de contrôle sont à|1⟩.
from qiskit import QuantumCircuit
# Créer un sous-circuit et obtenir ses variantes
qc_orig = QuantumCircuit(2, name="prep")
qc_orig.h(0)
qc_orig.cx(0, 1)
qc_orig.t(0)
porte = qc_orig.to_gate()
# .inverse() — l'opération inverse (le « undo »)
porte_inv = porte.inverse()
# Applique : Adjoint(T) puis CNOT puis H — l'ordre est inversé
# .control(1) — version contrôlée par un qubit
porte_ctrl = porte.control(1)
# Le circuit ne s'applique que si le qubit de contrôle est à |1⟩
# Assembler le tout
circuit = QuantumCircuit(4)
circuit.append(porte, [0, 1]) # appliquer la porte
circuit.append(porte_inv, [0, 1]) # annuler (revient à l'identité)
circuit.append(porte_ctrl, [2, 0, 1]) # version contrôlée par q2
print(circuit.draw())
| Méthode | Effet | Analogie |
|---|---|---|
gate.inverse() | Renvoie l’inverse (adjoint) | Le « Ctrl+Z » d’une porte |
gate.control(n) | Version contrôlée par n qubits | if (ctrl == |1⟩) apply(gate) |
circuit.to_gate() | Convertit un circuit en porte réutilisable | Encapsuler du code dans une fonction |
Gestion des qubits : QuantumRegister et AncillaRegister
Dans un circuit complexe, tu manipules souvent des groupes de qubits avec des rôles différents : qubits de données, qubits auxiliaires (ancillas), qubits de syndrome… Qiskit fournit deux types de registres pour structurer cela :
QuantumRegister(n, name)— allouenqubits destinés au calcul principal.AncillaRegister(n, name)— allouenqubits auxiliaires (ancillas). Ce sont des qubits « de travail » utilisés temporairement dans un calcul, puis nettoyés. Le transpileur peut les traiter différemment pour optimiser le mapping.
from qiskit import QuantumCircuit, QuantumRegister, AncillaRegister, ClassicalRegister
data = QuantumRegister(2, "data")
ancilla = AncillaRegister(1, "anc")
cr = ClassicalRegister(2, "out")
qc = QuantumCircuit(data, ancilla, cr)
qc.h(data[0])
qc.cx(data[0], ancilla[0]) # utilise l'ancilla comme qubit auxiliaire
qc.cx(ancilla[0], data[1])
qc.reset(ancilla[0]) # nettoyage explicite de l'ancilla
qc.measure(data, cr)
Point clé : contrairement à certains langages quantiques qui garantissent automatiquement l’état initial des qubits et exigent leur nettoyage à la sortie d’un bloc, en Qiskit c’est le programmeur qui gère le cycle de vie des qubits. Il faut penser à
reset()les ancillas après usage et à vérifier que les qubits auxiliaires ne « polluent » pas le reste du calcul. L’AncillaRegisterest une convention sémantique qui aide le transpileur et la lisibilité, mais ne force pas de contrainte d’exécution.
L’écosystème Qiskit
Qiskit n’est pas un monolithe — c’est un écosystème de packages organisé autour de trois piliers :
qiskit(anciennement Terra) : le cœur —QuantumCircuit, le transpileur,quantum_info(matrices, opérateurs), et la bibliothèque de portes standard.qiskit-aer: les simulateurs hautes performances locaux.AerSimulatorsimule des circuits avec ou sans modèle de bruit,StatevectorSimulatordonne le vecteur d’état exact, et tu peux même simuler les erreurs de vrais QPU.qiskit-ibm-runtime: la connexion aux vrais processeurs quantiques IBM et aux primitives cloud (Sampler,Estimator). C’est le pont entre ton circuit local et l’exécution sur du matériel réel.
Point clé : le workflow moderne est : écrire un circuit (
qiskit) → transpiler vers la machine cible (qiskit, avectranspile()) → soumettre via les primitives Runtime (qiskit-ibm-runtime). Pour le prototypage,qiskit-aerpermet de tout tester localement avec des simulateurs fidèles, y compris avec du bruit réaliste.
Qiskit en profondeur
Qiskit est le SDK quantique d’IBM, en Python — ton autre langage natif. Tu écris un QuantumCircuit abstrait avec des portes idéales… mais une vraie machine ne « parle » pas toutes les portes ni n’a une connectivité parfaite. C’est là qu’intervient la transpilation.
La transpilation — compiler pour une machine réelle
La transpilation est l’étape qui transforme ton circuit idéal en un circuit réellement exécutable sur une cible donnée. C’est l’équivalent quantique d’un compilateur qui traduit ton Python en instructions machine selon la cible.
Elle fait trois choses principales :
- Traduire les portes vers le jeu supporté par la machine (les basis gates).
- Router les qubits : si la machine ne permet pas une porte à 2 qubits entre deux qubits non voisins, le transpileur insère des portes SWAP pour les rapprocher.
- Optimiser : fusionner des portes, annuler les paires inverses, simplifier.
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1) # porte abstraite "CX"
backend = AerSimulator()
# Transpiler vers un jeu de portes concret + optimisation niveau 2
qc_t = transpile(qc, backend,
basis_gates=['rz', 'sx', 'x', 'cx'],
optimization_level=2)
print(qc_t.count_ops()) # ex. {'rz': 4, 'sx': 2, 'cx': 1}
Les basis gates — l’alphabet de la machine
Une machine physique n’implémente qu’un petit jeu de portes : son basis gates. Toutes les autres portes doivent être décomposées dans cet alphabet. Un jeu typique pour les machines IBM supraconductrices : ['rz', 'sx', 'x', 'cx'].
Analogie : c’est comme un jeu d’instructions CPU. Ton code de haut niveau utilise « multiplier deux matrices », mais le processeur ne connaît que
ADD,MUL,MOV. Le compilateur (transpileur) traduit. Une porte Hadamard n’existe pas physiquement sur la puce IBM : elle est recompilée enrz+sx.
Piège : croire que le nombre de portes de ton circuit abstrait reflète le coût réel. Après transpilation vers les basis gates d’une vraie machine, une porte « simple » peut exploser en plusieurs portes natives — et chaque porte ajoute du bruit.
Les niveaux d’optimisation
transpile(..., optimization_level=N) accepte N de 0 à 3 :
| Niveau | Comportement | Quand l’utiliser |
|---|---|---|
0 | Traduction minimale, aucune optimisation | Débogage, reproductibilité exacte |
1 | Optimisations légères (défaut) | Usage courant |
2 | Optimisations moyennes (routing soigné) | Bon compromis |
3 | Optimisations agressives (lent à transpiler) | Circuit final à exécuter sur du vrai matériel |
Point clé : plus le niveau est haut, plus la transpilation prend de temps, mais plus le circuit résultant est court (moins de portes → moins de bruit). Le niveau 3 peut diviser par 2 le nombre de portes à 2 qubits — décisif sur du matériel bruité.
Les primitives Sampler et Estimator
Qiskit moderne pousse à ne plus appeler backend.run() directement, mais à passer par deux primitives qui couvrent 95 % des besoins :
Sampler: tu lui donnes un circuit avec mesures, il te rend la distribution des chaînes de bits mesurées (les « quasi-probabilités »). Utile quand tu veux l’histogramme des résultats (ex. Grover, échantillonnage).Estimator: tu lui donnes un circuit sans mesure + un observable (ce que tu veux mesurer comme grandeur physique, ex. une énergie), il te rend une valeur d’espérance (un nombre). C’est exactement ce dont VQE a besoin pour évaluer⟨ψ(θ)|H|ψ(θ)⟩.
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import StatevectorEstimator, StatevectorSampler
# --- Sampler : distribution de bits ---
qc = QuantumCircuit(2)
qc.h(0); qc.cx(0, 1)
qc.measure_all()
sampler = StatevectorSampler()
res = sampler.run([(qc,)], shots=2000).result()
print(res[0].data.meas.get_counts()) # ~{'00': 1000, '11': 1000}
# --- Estimator : valeur d'espérance d'un observable ---
qc2 = QuantumCircuit(2)
qc2.h(0); qc2.cx(0, 1) # PAS de mesure pour l'Estimator
obs = SparsePauliOp("ZZ") # observable : Z⊗Z
estimator = StatevectorEstimator()
val = estimator.run([(qc2, obs)]).result()
print(val[0].data.evs) # ≈ 1.0 (état de Bell : Z⊗Z vaut +1)
Piège fréquent : ajouter un
measureau circuit donné à l’Estimator. L’Estimator gère la mesure de l’observable lui-même ; il attend un circuit qui prépare l’état, pas qui le mesure. À l’inverse, leSamplerexige des mesures.
Le runtime IBM Quantum
Le Qiskit Runtime est le service cloud d’IBM qui exécute tes primitives au plus près du matériel. Au lieu d’aller-retours réseau entre ton laptop et le QPU pour chaque itération (catastrophique pour VQE/QAOA), tu envoies un job qui tourne dans un environnement collé à la machine.
Analogie : c’est la différence entre faire N appels HTTP séparés à une base de données distante (chaque appel paie la latence réseau), et envoyer une procédure stockée qui fait toute la boucle côté serveur. Le Runtime, c’est la « procédure stockée » de l’optimisation variationnelle : la boucle quantique-classique tourne près du QPU.
Point clé : sur du vrai matériel IBM, le chemin moderne est : écrire le circuit →
transpile(niveau 3, vers les basis gates de la machine) → soumettre viaSampler/Estimatordu Runtime. La transpilation locale et l’exécution distante sont deux étapes distinctes.