Expert Chapitre 53-54 / 27

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 QuantumCircuit est 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 via qubits=[...]). 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 les n qubits de contrôle valent tous |1⟩. Par exemple RYGate(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') : regroupe n qubits sous un nom logique. Un circuit peut contenir plusieurs registres (ex. qr_data pour les données, qr_anc pour 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 à un QuantumRegister ordinaire. Tous les qubits démarrent à |0⟩.

L’écosystème Qiskit

ComposantRôle
qiskit (ex qiskit-terra)Cœur : QuantumCircuit, transpileur, passes d’optimisation
qiskit-aerSimulateur haute performance (statevector, QASM, bruit réaliste)
qiskit-ibm-runtimeAccès au matériel IBM Quantum, primitives Estimator / Sampler
qiskit.transpilerTransforme 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.
AxeAnsatz hardware-efficientAnsatz problème-aware
ExpressibilitéTrès élevéeRestreinte au problème
Barren plateausFréquents (n grand)Atténués
EntraînabilitéFaible à grande échelleMeilleure
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.


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.