Expert Chapitre 34-35 / 33

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é : QuantumCircuit est 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 par n qubits. 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éthodeEffetAnalogie
gate.inverse()Renvoie l’inverse (adjoint)Le « Ctrl+Z » d’une porte
gate.control(n)Version contrôlée par n qubitsif (ctrl == |1⟩) apply(gate)
circuit.to_gate()Convertit un circuit en porte réutilisableEncapsuler 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) — alloue n qubits destinés au calcul principal.
  • AncillaRegister(n, name) — alloue n qubits 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’AncillaRegister est 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. AerSimulator simule des circuits avec ou sans modèle de bruit, StatevectorSimulator donne 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, avec transpile()) → soumettre via les primitives Runtime (qiskit-ibm-runtime). Pour le prototypage, qiskit-aer permet 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 :

  1. Traduire les portes vers le jeu supporté par la machine (les basis gates).
  2. 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.
  3. 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 en rz + 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 :

NiveauComportementQuand l’utiliser
0Traduction minimale, aucune optimisationDébogage, reproductibilité exacte
1Optimisations légères (défaut)Usage courant
2Optimisations moyennes (routing soigné)Bon compromis
3Optimisations 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 measure au 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, le Sampler exige 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 via Sampler/Estimator du Runtime. La transpilation locale et l’exécution distante sont deux étapes distinctes.


Quiz — teste tes connaissances
Expert 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Remonte relire la fiche memo ci-dessus en pretant attention aux points rates, puis clique sur « Recommencer » pour retenter.