Qiskit : circuits, registres et portes personnalisées & Compilation et synthèse de circuits
Construire des circuits modulaires en Qiskit (registres, portes personnalisées, inverse, control) et comprendre la compilation quantique (KAK, Solovay-Kitaev, routing) — sans prérequis mathématiques, avec du code Qiskit.
Qiskit : circuits, registres et portes personnalisées
Qiskit organise un circuit quantique autour de registres nommés et de portes réutilisables. Si vous venez du monde .NET/Python, pensez aux registres comme des variables typées et aux portes personnalisées comme des méthodes extraites — on structure le code pour le rendre lisible, testable et composable.
Registres : organiser ses qubits et bits classiques
QuantumRegister(n, 'nom')regroupenqubits sous un nom logique. Un circuit peut contenir plusieurs registres :qr_datapour les données,qr_ctrlpour les contrôles. Analogie C# : c’est comme déclarer des variables nommées plutôt que d’utiliser un tableau anonyme — on sait tout de suite quel qubit fait quoi.ClassicalRegister(n, 'nom')stocke les résultats de mesure dansnbits classiques nommés. Indispensable pour le contrôle conditionnel (c_if) ou simplement pour savoir quel bit correspond à quelle mesure.AncillaRegister(n, 'nom')est un registre spécial pour les qubits ancillaires (de travail temporaire). Le transpileur Qiskit sait qu’il peut réutiliser ou optimiser ces qubits, contrairement à unQuantumRegisterordinaire. Tous les qubits démarrent à|0>.
Portes personnalisées : to_gate(), compose(), append()
sub_circuit.to_gate(label="...")convertit un sous-circuit en une porte réutilisable. Analogie OOP : c’est exactement le refactoring “Extract Method” — on prend un bloc de code, on en fait une fonction nommée qu’on appelle partout.qc.append(gate, qubits)ajoute une porte (standard ou personnalisée) à un circuit. C’est l’ajout ponctuel — on place la porte sur les qubits voulus.qc.compose(other, qubits=[...])colle un circuit entier à la suite. C’est la composition modulaire — chaque couche peut être un sous-circuit indépendant qu’on assemble. Contrairement àappend,composefusionne le circuit inline plutôt que de l’encapsuler dans une boîte.
Transformations : inverse() et control(n)
gate.inverse()renvoie la porte inverse (son “annuler”). Appliquer U puis U.inverse() = identité. Indispensable pour le pattern compute—uncompute (Grover, QPE, nettoyage d’ancillas).gate.control(n)ajoutenqubits de contrôle : l’opération ne s’applique que si lesncontrôles valent tous|1>. Exemple : sur une porte 2-qubits,.control(2)produit une porte à 4 qubits (2 contrôles + 2 cibles).
Simulation locale avec AerSimulator
AerSimulator(paquetqiskit-aer) permet de simuler un circuit localement — vecteur d’état exact, simulation QASM avec shots, ou modèle de bruit réaliste. Pas besoin d’accès à un backend IBM pour tester son code.
| Méthode / Classe | Rôle | Analogie dev |
|---|---|---|
QuantumRegister | Groupe nommé de qubits | Variable typée |
ClassicalRegister | Bits classiques pour mesures | Variable de retour |
AncillaRegister | Qubits temporaires optimisables | Variable locale jetable |
.to_gate() | Sous-circuit vers porte réutilisable | Extract Method |
.append() | Ajoute une porte au circuit | Appel de méthode |
.compose() | Fusionne un circuit entier | Inlining de méthode |
.inverse() | Porte inverse (“annuler”) | Undo / rollback |
.control(n) | Ajoute n qubits de contrôle | Garde conditionnelle (if) |
# Qiskit — registres nommés et construction de circuit
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, AncillaRegister
# Registres nommés : on sait immédiatement quel qubit fait quoi
qr_data = QuantumRegister(2, "data") # 2 qubits de données
qr_anc = AncillaRegister(1, "ancilla") # 1 qubit de travail temporaire
cr = ClassicalRegister(2, "result") # 2 bits classiques pour les mesures
qc = QuantumCircuit(qr_data, qr_anc, cr)
# Construction du circuit
qc.h(qr_data[0]) # Hadamard sur data[0]
qc.cx(qr_data[0], qr_data[1]) # CNOT : intrication data[0] → data[1]
qc.cx(qr_data[0], qr_anc[0]) # ancilla utilisée comme travail temporaire
qc.measure(qr_data, cr) # mesure data → result
print(qc.draw())
# Les registres nommés rendent le circuit lisible comme du code bien structuré
# Qiskit — to_gate(), control(), inverse() et le pattern compute-uncompute
from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister
# 1. Créer une sous-routine réutilisable
sub = QuantumCircuit(2, name="encode")
sub.h(0)
sub.cx(0, 1)
encode_gate = sub.to_gate() # « Extract Method » → porte réutilisable
# 2. Version contrôlée : 1 qubit de contrôle + 2 cibles = 3 qubits
c_encode = encode_gate.control(1) # applique encode seulement si ctrl = |1⟩
# 3. Version inverse pour « uncompute »
decode_gate = encode_gate.inverse() # annule encode → retour à l'état initial
# 4. Assemblage dans un circuit avec registres
qr = QuantumRegister(2, "q")
anc = AncillaRegister(1, "ctrl")
qc = QuantumCircuit(qr, anc)
qc.h(anc[0]) # superposition du qubit de contrôle
qc.append(c_encode, [anc[0], qr[0], qr[1]]) # encode contrôlé par l'ancilla
qc.append(decode_gate, [qr[0], qr[1]]) # uncompute : nettoie les données
print(qc.draw())
# .control(2) sur encode_gate donnerait une porte à 4 qubits (2 ctrl + 2 cibles)
Attention.
.inverse()échoue si le circuit contient une mesure ou un reset : ces opérations sont irréversibles. Retirez les mesures avant d’appeler.inverse(). De même,.to_gate()ne fonctionne que sur un circuit sans bits classiques (pas de mesure dans le sous-circuit).
Compilation et synthèse de circuits
Un circuit logique (Toffoli, portes multi-qubits, rotations quelconques) ne peut pas s’exécuter tel quel sur un processeur quantique réel. Le compilateur quantique (transpileur) le transforme en instructions natives du matériel. C’est la même idée qu’un compilateur C# vers IL vers code machine : chaque couche réduit l’abstraction vers ce que le matériel sait réellement faire.
Décomposition KAK
- Toute porte 2-qubits peut être décomposée en au maximum 3 portes CNOT plus des rotations locales (portes 1-qubit). C’est le résultat de la décomposition KAK (Khaneja—Glaser). En pratique, beaucoup de portes 2-qubits se décomposent en seulement 0, 1 ou 2 CNOT.
- Analogie : c’est comme réduire n’importe quelle opération matricielle complexe en une séquence de quelques opérations élémentaires — on ne perd rien, on reformule pour la machine.
Synthèse de Solovay—Kitaev
- Le théorème de Solovay—Kitaev garantit qu’on peut approximer n’importe quelle porte 1-qubit à précision epsilon à partir d’un ensemble fini de portes (ex. H, T), avec seulement O(log^c(1/epsilon)) portes (c environ 3,97). La complexité est poly-logarithmique en 1/epsilon — on n’a pas besoin de beaucoup de portes même pour une très grande précision.
- Analogie : avec seulement deux briques LEGO de base, on peut reconstruire n’importe quelle forme à une précision arbitraire — il suffit de savoir comment les empiler.
Optimisation par patterns
- Le transpileur reconnaît des identités algébriques et simplifie :
H*H = I(deux Hadamard consécutifs s’annulent),CNOT*CNOT = I, fusion de rotationsRz(alpha)*Rz(beta) = Rz(alpha+beta), commutation de portes indépendantes pour créer des annulations. - Analogie : c’est le peephole optimizer d’un compilateur classique — il repère les séquences inutiles et les élimine.
Qubit routing et insertion de SWAP
- Sur un vrai processeur, les qubits ne sont pas tous connectés entre eux : un CNOT ne peut s’appliquer qu’entre qubits physiquement voisins. Si un CNOT logique cible deux qubits éloignés, le transpileur insère des portes SWAP pour rapprocher les données — chaque SWAP coûte 3 portes CNOT.
- Analogie : c’est exactement un allocateur de registres dans un compilateur CPU. Quand deux variables doivent interagir mais sont dans des registres éloignés, on insère des
MOV(ici des SWAP) pour les rapprocher. Le coût est réel — chaque SWAP ajoute du bruit.
Niveaux de transpilation dans Qiskit (0—3)
| Niveau | Optimisations | Temps |
|---|---|---|
optimization_level=0 | Aucune optimisation, juste mapping et décomposition en portes natives | Le plus rapide |
optimization_level=1 | Réduction de portes, fusion de rotations simples | Rapide |
optimization_level=2 | Commutation de portes, annulations, meilleur routing | Modéré |
optimization_level=3 | Toutes les passes, synthèse unitaire, essais multiples de layout | Le plus lent |
# Qiskit — transpilation d'un Toffoli (CCX) vers un backend réel
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_aer import AerSimulator
# Circuit logique avec un Toffoli (3-qubit : 2 contrôles + 1 cible)
qc = QuantumCircuit(3)
qc.h(2)
qc.ccx(0, 1, 2) # Porte Toffoli — n'existe pas nativement sur la plupart des backends
qc.measure_all()
print("Circuit logique :")
print(qc.draw())
print(f" Portes : {qc.count_ops()}")
# Transpilation niveau 0 vs niveau 3
backend = AerSimulator()
for level in [0, 3]:
pm = generate_preset_pass_manager(optimization_level=level, backend=backend)
qc_t = pm.run(qc)
ops = qc_t.count_ops()
cx_count = ops.get('cx', 0)
print(f"\nNiveau {level} : {sum(ops.values())} portes, dont {cx_count} CX")
# Niveau 0 : ~15 portes, ~6 CX (décomposition brute du Toffoli)
# Niveau 3 : souvent moins de CX grâce aux optimisations
Le coût caché des SWAP. Chaque porte SWAP insérée par le routing coûte 3 portes CNOT. Sur un processeur à connectivité limitée (topologie en ligne ou en grille), un circuit de 10 CNOT logiques peut facilement devenir 30+ CNOT physiques. C’est la principale source de bruit sur le matériel actuel. Un
optimization_levelplus élevé ne garantit pas strictement moins de portes — il essaie davantage de stratégies mais le résultat dépend du circuit et de la topologie.