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
functionQ# comme à une méthode pure en C# —[Pure], déterministe, sans effet de bord. Et à uneoperationcomme à une méthode qui touche à de l’état mutable ou à de l’I/O : elle « fait quelque chose » au monde.
- Une
functionest purement classique et déterministe : mêmes entrées → mêmes sorties, toujours. Elle ne peut pas toucher à un qubit, ni appelerM(mesure), ni générer de l’aléatoire. Exemple : calculerSin(x), trier un tableau. - Une
operationpeut 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. AppelerH(q)ouM(q)n’est possible que dans uneoperation.
// 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)ouH(q)depuis unefunction. Le compilateur refuse. Unefunctionne peut jamais voir unQubit.
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éthodeUndo()correspondant à ta méthodeDo(). 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é | Effet | Analogie |
|---|---|---|
Adjoint Op(...) | Applique l’inverse de Op | Op.Undo() |
Controlled Op([c], ...) | Op ne s’applique que si c est à |1⟩ | if (c) Op() |
is Adj | L’op promet de fournir son inverse | implémente IReversible |
is Adj + Ctl | Inverse et version contrôlée | IReversible + 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⟩(viaReset/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 unArrayPoolsans pouvoir présumer de son contenu, et le rendre intact. Leborrowest 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/ResetAllavant la fin d’un blocuse. 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
.qscompilé avecdotnet build, piloté depuis un programme C# hôte (qui prépare les entrées classiques et lit lesResult). - 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 :
- Traduire les portes vers le jeu supporté par la machine (les basis gates).
- 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.
- 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 enrz+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 :
| Niveau | Comportement | Quand l’utiliser |
|---|---|---|
0 | Traduction minimale, aucune optimisation | Débogage, reproductibilité exacte |
1 | Optimisations légères (défaut) | Usage courant |
2 | Optimisations moyennes (routing soigné) | Bon compromis |
3 | Optimisations 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
measureau 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, leSamplerexige 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 viaSampler/Estimatordu Runtime. La transpilation locale et l’exécution distante sont deux étapes distinctes.