Qiskit en profondeur & Avantages et limites des PQC paramétrés
Révision Expert : ouvrir le capot de Qiskit (construction de circuits, .inverse(), .control(), registres, écosystème Aer/IBM Runtime/transpilation) puis cerner les limites des circuits paramétrés (PQC) — barren plateaus, compromis expressibilité/entraînabilité, ansatz problème-aware — sans prérequis mathématiques, avec du code Qiskit.
Qiskit en profondeur
Qiskit est le framework open-source d’IBM pour le calcul quantique. Il permet de construire, simuler, transpiler et exécuter des circuits sur du vrai matériel IBM Quantum. Cette fiche ouvre le capot sur quatre axes que tout développeur quantique doit maîtriser.
Construction et composition de circuits
- Un
QuantumCircuitest l’objet central : il porte des qubits, des bits classiques et une séquence d’instructions (portes, mesures, barrières). .append(gate, qubits)ajoute une porte (standard ou personnalisée) à un circuit existant..compose(other)colle un circuit entier à la suite d’un autre (ou sur un sous-ensemble de qubits viaqubits=[...]). C’est la brique de base pour construire des circuits modulaires — chaque couche peut être un sous-circuit indépendant.- Pour créer une porte personnalisée, on construit un petit
QuantumCircuit, puis on appelle.to_gate(label="...")pour le convertir en une porte réutilisable.
.inverse() et .control() — les transformations de portes
gate.inverse()renvoie la porte inverse (son « annuler »). Pour une porte unitaire U, appliquer U puis U.inverse() revient à l’identité. Utile pour le « uncompute » dans les algorithmes de Grover, QPE, etc.gate.control(n)renvoie une version contrôlée de la porte : l’opération ne s’applique que si lesnqubits de contrôle valent tous |1⟩. Par exempleRYGate(0.5).control(1)donne un CRY.- Le transpileur de Qiskit décompose ensuite ces portes en portes natives du matériel cible (CX, √X, Rz, etc.). On ne se soucie pas de la décomposition à la main.
⚠️ Piège classique.
.inverse()échoue si le circuit contient une mesure ou un reset : ces opérations sont irréversibles, Qiskit ne peut pas construire l’inverse. Il faut retirer les mesures avant d’appeler.inverse().
Registres : QuantumRegister, ClassicalRegister, AncillaRegister
QuantumRegister(n, 'nom'): regroupenqubits sous un nom logique. Un circuit peut contenir plusieurs registres (ex.qr_datapour les données,qr_ancpour les ancillas).ClassicalRegister(n, 'nom'): bits classiques pour stocker les résultats de mesure.AncillaRegister(n, 'nom'): registre dédié aux qubits ancillaires (qubits de travail temporaires). Le transpileur sait qu’il peut les réutiliser ou les optimiser — contrairement à unQuantumRegisterordinaire. Tous les qubits démarrent à |0⟩.
L’écosystème Qiskit
| Composant | Rôle |
|---|---|
| qiskit (ex qiskit-terra) | Cœur : QuantumCircuit, transpileur, passes d’optimisation |
| qiskit-aer | Simulateur haute performance (statevector, QASM, bruit réaliste) |
| qiskit-ibm-runtime | Accès au matériel IBM Quantum, primitives Estimator / Sampler |
| qiskit.transpiler | Transforme un circuit logique en circuit physique adapté au backend cible |
Le flux typique : on construit un QuantumCircuit → on le transpile pour un backend (transpile(qc, backend, optimization_level=2)) → on l’exécute via une primitive (Estimator pour les valeurs d’expectation, Sampler pour les distributions de probabilité).
# Qiskit — construction, composition, .inverse(), .control() et registres
from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister
from qiskit.circuit.library import RYGate
# --- Construction d'une sous-routine réutilisable ---
sub = QuantumCircuit(2, name="ma_routine")
sub.ry(0.5, 0)
sub.cx(0, 1)
ma_porte = sub.to_gate() # convertir en porte réutilisable
# --- .inverse() et .control() ---
ma_porte_inv = ma_porte.inverse() # annule la routine
ma_porte_ctrl = ma_porte.control(1) # version contrôlée (1 qubit de contrôle)
# --- Registres nommés ---
qr_data = QuantumRegister(2, "data")
qr_anc = AncillaRegister(1, "ancilla")
qc = QuantumCircuit(qr_data, qr_anc)
qc.append(ma_porte_ctrl, [qr_anc[0], qr_data[0], qr_data[1]])
# ancilla contrôle l'application de ma_routine sur data[0], data[1]
qc.append(ma_porte_inv, [qr_data[0], qr_data[1]])
# uncompute : annule la routine sur data
print(qc.draw())
# Qiskit — écosystème : transpilation + exécution via primitives
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
from qiskit.quantum_info import SparsePauliOp
# Circuit simple
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
# Transpilation pour un backend réel
service = QiskitRuntimeService()
backend = service.least_busy(min_num_qubits=2, simulator=False)
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
qc_transpiled = pm.run(qc)
# Exécution : Estimator calcule une valeur d'expectation
observable = SparsePauliOp("ZZ")
estimator = EstimatorV2(mode=backend)
job = estimator.run([(qc_transpiled, observable)])
result = job.result()
print(f"⟨ZZ⟩ = {result[0].data.evs}") # valeur d'expectation
Avantages et limites des PQC paramétrés
Un circuit quantique paramétré (PQC) est un circuit dont les angles de rotation θ sont réglables. C’est le « modèle » au cœur de VQE, QAOA et du machine learning quantique : un optimiseur classique ajuste θ pour minimiser un coût (ex. l’énergie ⟨ψ(θ)|H|ψ(θ)⟩). Puissant — mais avec des limites profondes.
Barren plateaus (plateaux stériles)
- Quand le nombre de qubits grandit (et que l’ansatz est profond/aléatoire), le paysage de coût devient exponentiellement plat presque partout : le gradient s’annule exponentiellement avec le nombre de qubits.
- Analogie. Chercher le point le plus bas d’un désert parfaitement plat : aucune pente pour guider la descente, on erre au hasard. Estimer le moindre gradient demande alors un nombre exponentiel de mesures (shots).
Le compromis expressibilité ↔ entraînabilité
- Plus un ansatz est expressif (capable d’atteindre uniformément tout l’espace des états, proche d’un « 2-design »), plus il est sujet aux barren plateaus — donc plus dur à entraîner. Contre-intuitif quand on vient du deep learning classique.
- Un coût global (mesurer tous les qubits d’un coup) déclenche le plateau même à faible profondeur ; un coût local (mesurer peu de qubits) l’évite pour des circuits peu profonds.
📚 Repères : McClean et al. (2018) montrent l’annulation exponentielle du gradient ; Cerezo et al. (2021) relient le phénomène au caractère global du coût ; Holmes et al. (2022) lient expressibilité et platitude du paysage.
Ansatz « problème-aware »
- La parade : ne pas prendre un ansatz générique « hardware-efficient » aléatoire, mais un circuit qui encode la structure du problème (UCC en chimie, mélangeur de QAOA, symétries conservées). On restreint l’espace de recherche → gradients exploitables.
| Axe | Ansatz hardware-efficient | Ansatz problème-aware |
|---|---|---|
| Expressibilité | Très élevée | Restreinte au problème |
| Barren plateaus | Fréquents (n grand) | Atténués |
| Entraînabilité | Faible à grande échelle | Meilleure |
| Coût recommandé | — | Local plutôt que global |
# Qiskit — un ansatz « hardware-efficient » paramétré
from qiskit.circuit import QuantumCircuit, ParameterVector
n = 4
theta = ParameterVector("t", 2 * n) # 2n paramètres entraînables
qc = QuantumCircuit(n)
for i in range(n):
qc.ry(theta[i], i) # couche de rotations
for i in range(n - 1):
qc.cx(i, i + 1) # couche d'intrication
for i in range(n):
qc.ry(theta[n + i], i)
# Plus n grandit et plus l'ansatz est expressif/aléatoire,
# plus le gradient moyen s'annule exponentiellement -> barren plateau.
# Qiskit — ansatz paramétré avec ParameterVector et Estimator
from qiskit.circuit import QuantumCircuit, ParameterVector
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import StatevectorEstimator
import numpy as np
n = 4
theta = ParameterVector("θ", 2 * n)
qc = QuantumCircuit(n)
for i in range(n):
qc.ry(theta[i], i)
for i in range(n - 1):
qc.cx(i, i + 1)
for i in range(n):
qc.ry(theta[n + i], i)
# Observable : somme de termes ZZ sur paires voisines (modèle Ising simplifié)
obs = SparsePauliOp.from_list([("ZZII", 1), ("IZZI", 1), ("IIZZ", 1)])
# Évaluation avec des paramètres concrets
estimator = StatevectorEstimator()
params_init = np.random.uniform(0, np.pi, 2 * n)
job = estimator.run([(qc, obs, params_init)])
energie = job.result()[0].data.evs
print(f"Énergie initiale : {energie:.4f}")
# Boucle hybride : un optimiseur classique (COBYLA, SPSA…) ajuste theta
# pour minimiser cette énergie. Si barren plateau → l'optimiseur stagne.
⚠️ Un barren plateau n’est pas un minimum local. Un minimum local a une pente autour de lui ; un plateau stérile est plat : le gradient lui-même disparaît, impossible même de commencer la descente. Et empiler couches/qubits pour rendre le modèle « plus puissant » aggrave souvent le problème — l’inverse du réflexe deep learning.