Expert Chapitre 34-35 / 19

Q# en profondeur & Qiskit en profondeur

Maîtriser les deux SDK de référence — operation vs function, Adjoint/Controlled et allocation de qubits en Q#, transpilation, primitives Sampler/Estimator et runtime IBM en Qiskit — sans prérequis mathématiques.

Q# en profondeur

Tu connais le C# : un langage à types stricts, compilé, pensé pour la fiabilité. Q# (prononcé « Q-sharp ») est le langage quantique de Microsoft, et il a été conçu par les mêmes équipes que .NET, dans le même esprit. Si tu sais lire du C#, tu liras du Q#. La différence : Q# manipule des Qubit, pas des int.

operation vs function — la distinction qui structure tout

En Q#, il existe deux sortes de routines, et la frontière entre les deux est la chose la plus importante à comprendre.

Analogie .NET : Pense à une function Q# comme à une méthode pure en C# — [Pure], déterministe, sans effet de bord. Et à une operation comme à une méthode qui touche à de l’état mutable ou à de l’I/O : elle « fait quelque chose » au monde.

  • Une function est purement classique et déterministe : mêmes entrées → mêmes sorties, toujours. Elle ne peut pas toucher à un qubit, ni appeler M (mesure), ni générer de l’aléatoire. Exemple : calculer Sin(x), trier un tableau.
  • Une operation peut agir sur des qubits, les mesurer, appeler d’autres opérations. C’est le seul endroit où le « quantique » a le droit de se produire. Appeler H(q) ou M(q) n’est possible que dans une operation.
// function : purement classique, déterministe
function CarreClassique(x : Int) : Int {
    return x * x;
}

// operation : agit sur un qubit, donc non déterministe (la mesure)
operation TirerPileOuFace() : Result {
    use q = Qubit();   // alloue un qubit à |0⟩
    H(q);              // superposition 50/50
    let r = M(q);      // mesure → effondre l'état (aléatoire)
    Reset(q);          // remet à |0⟩ avant libération (obligatoire)
    return r;
}

Piège classique : vouloir appeler M(q) ou H(q) depuis une function. Le compilateur refuse. Une function ne peut jamais voir un Qubit.

Adjoint et Controlled — des transformations gratuites

C’est la fonctionnalité la plus élégante de Q#. À partir d’une opération, le compilateur peut dériver automatiquement deux variantes :

  • Adjoint : l’opération inverse (le « undo »). Comme toute porte quantique est réversible, Q# sait construire l’inverse pour toi. Adjoint H = appliquer H à l’envers.
  • Controlled : la version contrôlée par un (ou plusieurs) qubit(s). Controlled X([ctrl], cible) = un CNOT.

Analogie : imagine un attribut C# [GenerateInverse] qui, à la compilation, écrirait pour toi la méthode Undo() correspondant à ta méthode Do(). Tu écris l’aller, le compilateur t’offre le retour — et la version « conditionnelle » en prime.

Pour que ces variantes soient disponibles, l’opération doit déclarer ce qu’elle sait faire via is Adj + Ctl :

// Cette opération est "adjointable" ET "contrôlable"
operation PrepareEtat(q : Qubit) : Unit is Adj + Ctl {
    H(q);
    T(q);
}

operation Demo() : Unit {
    use (a, b) = (Qubit(), Qubit());
    PrepareEtat(a);                 // applique H puis T
    Adjoint PrepareEtat(a);         // applique l'inverse : Adjoint T puis H
    Controlled PrepareEtat([b], a); // version contrôlée par b
    ResetAll([a, b]);
}
Mot-cléEffetAnalogie
Adjoint Op(...)Applique l’inverse de OpOp.Undo()
Controlled Op([c], ...)Op ne s’applique que si c est à |1⟩if (c) Op()
is AdjL’op promet de fournir son inverseimplémente IReversible
is Adj + CtlInverse et version contrôléeIReversible + IControllable

Allouer des qubits : use et borrow

Les qubits sont une ressource gérée, comme un IDisposable.

  • use q = Qubit(); alloue un qubit garanti à |0⟩. À la sortie du bloc, Q# exige qu’il soit remis à |0⟩ (via Reset/mesure), sinon erreur d’exécution.
  • borrow q = Qubit(); « emprunte » un qubit qui peut être dans n’importe quel état (il appartient à un autre calcul, momentanément inutilisé). Tu dois le rendre exactement dans l’état où tu l’as trouvé. C’est une optimisation mémoire : réutiliser un qubit occupé plutôt que d’en allouer un neuf.

Analogie .NET : use = new + using (...) (tu obtiens un objet propre, tu le nettoies en sortant). borrow = emprunter un buffer dans un ArrayPool sans pouvoir présumer de son contenu, et le rendre intact. Le borrow est rare et réservé à l’optimisation fine.

operation ExempleAllocation() : Unit {
    use registre = Qubit[3];        // 3 qubits à |0⟩
    H(registre[0]);
    CNOT(registre[0], registre[1]);
    ResetAll(registre);             // obligatoire avant la fin du bloc
}

Piège : oublier Reset/ResetAll avant la fin d’un bloc use. En simulation, Q# lève une exception « qubit not in zero state ». Sur du vrai matériel, c’est une fuite de ressource.

Le QDK et l’intégration .NET / Jupyter

Le Quantum Development Kit (QDK) est l’ensemble outil : compilateur Q#, simulateurs, et bibliothèques standard. Trois façons de l’utiliser :

  • Projet .NET : un fichier .qs compilé avec dotnet build, piloté depuis un programme C# hôte (qui prépare les entrées classiques et lit les Result).
  • Jupyter (IQ#) : un noyau Jupyter qui exécute des cellules Q# interactives — idéal pour explorer.
  • Azure Quantum : soumettre le même code Q# à du vrai matériel (IonQ, Quantinuum…) ou à des simulateurs cloud.

Point clé : le découpage classique/quantique est volontaire. Le code C# hôte fait la logique « ordinaire » (boucles, I/O, décisions), Q# fait uniquement la partie quantique, et la communication se fait via des types simples (Int, Bool, Result). C’est le même hybride quantique-classique vu pour VQE/QAOA, mais au niveau du langage.


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 C# en instructions x86 ou ARM selon la machine.

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 .NET : 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.