Résonance magnétique nucléaire avec Qiskit
Pour ce module Qiskit in Classrooms, les étudiants doivent disposer d'un environnement Python fonctionnel avec les packages suivants installés :
qiskitv2.1 ou plus récentqiskit-ibm-runtimev0.40.1 ou plus récentqiskit-aerv0.17 ou plus récentscipyetmatplotlib(pour l'ajustement et le tracé)pylatexenc(pour le rendu des diagrammes de circuits)
Pour configurer et installer les packages ci-dessus, consulte le guide Installer Qiskit. Pour exécuter des jobs sur de vrais ordinateurs quantiques, les étudiants doivent créer un compte IBM Quantum® en suivant les étapes du guide Configurer ton compte IBM Cloud®.
Ce module a été testé et utilise environ 5 minutes de temps QPU. Il s'agit d'une estimation, et ton utilisation réelle peut varier.
Introduction
Lorsqu'un chimiste réalise une expérience de RMN, il place un échantillon de spins nucléaires dans un champ magnétique intense, applique une impulsion de radiofréquence (RF) et mesure le signal qui en revient. La technique repose sur quelques idées issues de la mécanique quantique d'une particule de spin 1/2 dans un champ magnétique. Le spin possède deux niveaux d'énergie séparés par une énergie qui correspond à une fréquence de Larmor caractéristique. Une impulsion résonante à cette fréquence bascule le spin dans le plan transverse, où il précesse et induit un signal qui s'atténue ensuite avec deux temps caractéristiques, et .
Un qubit supraconducteur est une version conçue du même objet : un système quantique à deux niveaux contrôlable. Ses états et sont l'analogue du spin haut et du spin bas, la porte que nous appliquons pour le faire tourner est l'analogue de l'impulsion RF, et il perd sa cohérence avec ses propres et . Ce n'est pas une simple comparaison approximative ; certains des tout premiers processeurs quantiques expérimentaux de la fin des années 1990 et du début des années 2000 utilisaient la RMN.
Dans ce module, tu vas utiliser un vrai ordinateur quantique pour exécuter les mêmes expériences de caractérisation que celles réalisées par un spectroscopiste RMN (ou un ingénieur quantique qui calibre du matériel) :
-
Rabi / nutation — piloter le spin et le regarder tourner. C'est ainsi que tu calibres une rotation de ou .
-
— relaxation spin-réseau, le retour d'un spin excité à l'équilibre.
-
— relaxation spin-spin (déphasage), mesurée avec une expérience de Ramsey et refocalisée avec un écho de Hahn.
-
Spectroscopie RMN (déplacement chimique et couplage ) — simuler une petite molécule sur plusieurs qubits et lire son spectre de multiplets pour retrouver la connectivité grâce à la règle .
Pour chaque concept, nous donnons d'abord la théorie minimale, puis nous implémentons l'expérience dans Qiskit en suivant le workflow en quatre étapes des Qiskit patterns.
Théorie
Particules de spin 1/2 dans un champ magnétique
Un noyau de spin 1/2 possède un moment magnétique , où est le rapport gyromagnétique. Dans un champ statique selon , les deux états de spin se séparent en énergie (l'effet Zeeman) :
L'écart d'énergie définit la fréquence de Larmor (de résonance) . Le moment magnétique précesse autour de à cette même fréquence.
Pour faire tourner le spin, nous appliquons un champ RF oscillant à (ou près de ). Dans le référentiel tournant à la fréquence de pilotage (le référentiel tournant), un pilotage résonant fait tourner le vecteur de Bloch autour d'un axe du plan -. L'ampleur de la rotation (l'angle de basculement) est fixée par l'intensité et la durée de l'impulsion. Deux choix comptent le plus : une impulsion de bascule un spin dans le plan transverse, et une impulsion de le fait tourner complètement jusqu'à .
Traduire entre le jargon de la RMN et celui de l'informatique quantique
Un qubit est un système à deux niveaux, mathématiquement identique à un spin 1/2 dans un champ magnétique — donc, même si nous travaillons sur un processeur quantique plutôt que sur un noyau, la physique est la même que celle qui sous-tend la RMN. Seul le vocabulaire diffère. Ce tableau t'aide à dialoguer aussi bien avec les physico-chimistes qu'avec les spécialistes de l'informatique quantique.
| RMN / spin 1/2 | Processeur quantique (qubit) |
|---|---|
| Spin haut / spin bas | / |
| Écart Zeeman | Énergie de transition du qubit |
| Fréquence de Larmor | Fréquence du qubit |
| Impulsion RF résonante (angle de basculement ) | Porte de rotation à un qubit |
| Impulsion de / | / |
| Relaxation spin-réseau | Temps de relaxation d'énergie du qubit |
| Relaxation spin-spin | Temps de déphasage du qubit |
| Nutation | Oscillations de Rabi |
| Décroissance libre d'induction (FID), écho de Hahn | Séquences de Ramsey, séquences d'écho |
Tout ce qui suit est un exercice consistant à mesurer la colonne de droite et à la lire comme la colonne de gauche.
Configurer l'environnement Qiskit
Nous suivons le workflow des Qiskit patterns tout au long du module :
-
Mapper le problème sur un circuit quantique.
-
Optimiser le circuit pour l'exécution (transpiler vers le backend).
-
Exécuter à l'aide d'une primitive IBM Quantum.
-
Post-traiter et analyser.
Nous chargeons d'abord le service runtime et choisissons le vrai backend le moins occupé. Si tu n'as pas encore enregistré tes identifiants, consulte la page de documentation expliquant comment enregistrer tes identifiants de connexion.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "scipy": "scipy"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
# Load the IBM Quantum Compute Service and select the least-busy real backend
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler, Batch
# After setup, this picks up your saved credentials automatically:
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=156)
print(f"Selected backend: {backend.name}")
Selected backend: ibm_kingston
Utilise la cellule ci-dessous pour basculer entre le vrai matériel et un simulateur local pour l'ensemble du notebook. L'exécution sur le matériel est l'objectif du module, mais en cas de problèmes de connectivité ou d'utilisation, le simulateur peut servir de solution de secours. Définis USE_HARDWARE = True pour exécuter sur le QPU sélectionné ci-dessus.
import numpy as np
from itertools import combinations
from qiskit_aer import AerSimulator
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
USE_HARDWARE = True # <-- set True to run on the QPU selected above
USE_BATCH = False # Safer for a class with pauses; set True for an uninterrupted run
if USE_HARDWARE:
run_backend = backend # the real QPU selected above
else:
# Local testing mode: an Aer simulator that mimics the selected device -- its noise model,
# coupling map, and qubit count -- while layout choices still use real calibration data.
run_backend = AerSimulator.from_backend(backend)
# --- Choose connected qubits with low readout error. --------------------------------------
def readout_error(q):
props = backend.target["measure"].get((q,))
return props.error if props is not None and props.error is not None else 1.0
coupling_map = backend.coupling_map or backend.target.build_coupling_map()
edges = {tuple(sorted(edge)) for edge in coupling_map.get_edges() if edge[0] != edge[1]}
# AX needs one connected pair.
AX_LAYOUT = list(min(edges, key=lambda edge: sum(readout_error(q) for q in edge)))
# AX2 needs one qubit connected to two neighbours; the first entry is the central A qubit.
neighbors = {q: set() for q in range(backend.num_qubits)}
for q0, q1 in edges:
neighbors[q0].add(q1)
neighbors[q1].add(q0)
ax2_layouts = (
[a, x1, x2]
for a, adjacent in neighbors.items()
for x1, x2 in combinations(adjacent, 2)
)
AX2_LAYOUT = min(ax2_layouts, key=lambda layout: sum(readout_error(q) for q in layout))
# Use a long-lived qubit for the single-qubit experiments.
def best_qubit(backend):
return max(
range(backend.num_qubits), key=lambda q: backend.qubit_properties(q).t2 or 0.0
)
QUBIT = best_qubit(backend)
qp = backend.qubit_properties(QUBIT)
print(
f"Using qubit {QUBIT} for single-qubit experiments: "
f"T1 = {qp.t1 * 1e6:.0f} us, T2 = {qp.t2 * 1e6:.0f} us"
)
print(f"Using physical qubits {AX_LAYOUT} for AX")
print(f"Using physical qubits {AX2_LAYOUT} for AX2 (A first, then the two X spins)")
pm_q = generate_preset_pass_manager(
optimization_level=1, backend=run_backend, initial_layout=[QUBIT]
)
pm_ax = generate_preset_pass_manager(
optimization_level=1, backend=run_backend, initial_layout=AX_LAYOUT
)
pm_ax2 = generate_preset_pass_manager(
optimization_level=1, backend=run_backend, initial_layout=AX2_LAYOUT
)
# Job mode is the default because a classroom pause can deactivate a Batch. On the Open plan,
# a Batch has a 10-minute maximum lifetime and deactivates after 1 minute with no queued job.
batch = Batch(backend=run_backend) if USE_BATCH else None
execution_mode = batch if batch is not None else run_backend
sampler = Sampler(mode=execution_mode)
estimator = Estimator(mode=execution_mode)
estimator.options.resilience_level = 0 # raw expectation values, no error mitigation
estimator.options.default_shots = 1000
mode_name = "Batch" if USE_BATCH else "individual jobs"
device_name = "hardware" if USE_HARDWARE else "local simulator (runtime local mode)"
print(f"Ready. Running on {device_name} using {mode_name}.")
Expérience : oscillations de Rabi (nutation)
Théorie
Pilote un spin à la résonance et son vecteur de Bloch tourne régulièrement d'un pôle à l'autre — il nute. Si nous appliquons une rotation d'angle de basculement autour de l'axe à un qubit initialement dans , la probabilité de le trouver dans est :
C'est la courbe de Rabi. Son premier maximum correspond à l'impulsion (inversion complète, l'impulsion de ) ; une impulsion est un quart de tour (l'impulsion de qui bascule un spin dans le plan transverse). En RMN, cette courbe sert à calibrer tes impulsions.
Une expérience de Rabi littérale balaie l'amplitude ou la durée du pilotage dans le temps. Cela nécessite un contrôle au niveau des impulsions, qui a été supprimé dans Qiskit v2.0. Ici, nous balayons à la place l'angle de rotation d'une porte . La courbe et l'identification de / sont identiques ; la seule différence est que nous choisissons directement un angle au lieu de calibrer une durée d'impulsion. Sur du matériel avec des portes fractionnaires, s'exécute nativement pour un arbitraire.
Une vue sur la sphère de Bloch
L'animation ci-dessous montre la nutation : une impulsion résonante fait tourner régulièrement le vecteur de Bloch autour de l'axe , le faisant passer de vers puis revenir. C'est exactement le mouvement que mesure la courbe de Rabi.
Associer le problème à un circuit
Nous construisons un circuit paramétré, suivi d'une mesure, puis nous l'évaluons ensuite pour de nombreux angles à la fois.
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
theta = Parameter("theta")
rabi = QuantumCircuit(1, 1)
rabi.rx(theta, 0)
rabi.measure(0, 0)
rabi.draw("mpl")
Optimiser pour l'exécution
Transpile vers un circuit d'architecture de jeu d'instructions (ISA) pour le backend choisi.
rabi_isa = pm_q.run(rabi)
Exécuter
Nous balayons de à (deux cycles de Rabi complets). Passer un tableau de valeurs de paramètres sous la forme d'un seul Primitive Unified Bloc (PUB) les exécute toutes dans un seul job.
angles = np.linspace(0, 4 * np.pi, 41)
job = sampler.run([(rabi_isa, angles.reshape(-1, 1))], shots=1000)
rabi_result = job.result()
Post-traiter et analyser
Extrais pour chaque angle et compare-le à .
import matplotlib.pyplot as plt
ba = rabi_result[0].data.c # BitArray, one entry per angle
shots = ba.num_shots
p1 = np.array([ba.get_counts(i).get("1", 0) / shots for i in range(len(angles))])
plt.figure(figsize=(7, 4))
plt.plot(angles, p1, "o", label="measured")
plt.plot(angles, np.sin(angles / 2) ** 2, "-", label=r"$\sin^2(\theta/2)$")
for k, name in [(np.pi / 2, r"$\pi/2$ (90°)"), (np.pi, r"$\pi$ (180°)")]:
plt.axvline(k, color="gray", ls="--", lw=1)
plt.text(k, 1.02, name, ha="center", fontsize=9)
plt.xlabel(r"flip angle $\theta$ (rad)")
plt.ylabel(r"$P_1$")
plt.title("Rabi / nutation curve")
plt.legend()
plt.tight_layout()
plt.show()

Vérifie ta compréhension
Sur la courbe que tu as mesurée, atteint son maximum pour la première fois à l'impulsion (). Suppose maintenant qu'un appareil soit mal calibré, de sorte qu'un angle nominal sur l'axe des x produise en réalité une rotation de .
(a) À quel angle nominal le premier maximum de apparaîtrait-il maintenant ? (b) Appliquer une seule impulsion serait-il un moyen sensible de détecter cette erreur de 5 % ?
Réponse
(a) Le maximum apparaît là où la rotation réelle est égale à , c'est-à-dire à l'angle nominal — un léger décalage du pic vers la gauche.
(b) Non. Une impulsion nominale produit une rotation réelle de , ce qui donne au lieu de — une variation trop petite pour être visible face au bruit de mesure. C'est pourquoi, pour calibrer précisément, les spectroscopistes RMN amplifient l'erreur en appliquant la rotation de nombreuses fois (par exemple la méthode , ou un long train d'impulsions), de sorte qu'une petite erreur par impulsion s'accumule en un écart important et mesurable. Nous revenons sur cette idée dans les problèmes de défi.
Expérience : (relaxation spin-réseau)
Théorie
Laissé à lui-même, un spin excité cède de l'énergie à son environnement (le « réseau ») et relaxe vers l'équilibre thermique. La population de l'état excité décroît de façon exponentielle avec le temps de relaxation spin-réseau :
Pour un chimiste, fixe le délai de récupération à attendre entre deux scans pour une RMN quantitative, et il renseigne sur la rotation moléculaire et la dynamique. Pour un qubit, c'est le temps de relaxation de l'énergie, et c'est l'un des deux principaux nombres utilisés pour modéliser les effets de décohérence dans un ordinateur quantique.
L'expérience. Prépare avec une porte (une impulsion ), attends un délai variable , puis mesure. La fraction encore dans dessine la décroissance.
Une vue sur la sphère de Bloch
La relaxation ramène l'état excité à l'équilibre, de sorte que le vecteur de Bloch glisse de vers le long de l'axe .
Construire et transpiler un balayage de délais
Les durées de délai doivent être des valeurs concrètes (elles sont planifiées en temps matériel), nous construisons donc un circuit par délai.
# Scale the T1 delay sweep to the qubit's reported coherence, and snap each delay to an integer
# number of samples (dt) so there are no "duration rounded" warnings (as in the Ramsey/echo cells).
T1_us = (backend.qubit_properties(QUBIT).t1 or 100e-6) * 1e6 # reported T1 (us)
delays_us = np.linspace(0, 5 * T1_us, 20) # 0 .. ~5 T1 captures the full decay
print(f"T1 sweep: 0..{5*T1_us:.0f} us in {len(delays_us)} points")
dt = backend.dt
t1_circuits = []
for tau in delays_us:
n_dt = int(round(tau * 1e-6 / dt)) # integer samples -> no rounding warning
qc = QuantumCircuit(1, 1)
qc.x(0) # pi pulse: prepare |1>
qc.delay(n_dt, 0, unit="dt") # wait tau
qc.measure(0, 0)
t1_circuits.append(qc)
t1_isa = pm_q.run(t1_circuits)
T1 sweep: 0..1393 us in 20 points
Exécuter
t1_job = sampler.run([(c,) for c in t1_isa], shots=1000)
t1_res = t1_job.result()
Analyser : ajuster l'exponentielle
from scipy.optimize import curve_fit
p1_t1 = np.array(
[r.data.c.get_counts().get("1", 0) / r.data.c.num_shots for r in t1_res]
)
def decay(t, A, T1, B):
return A * np.exp(-t / T1) + B
p0 = [1.0, 100.0, 0.0]
popt, _ = curve_fit(decay, delays_us, p1_t1, p0=p0, maxfev=10000)
T1_fit = popt[1]
plt.figure(figsize=(7, 4))
plt.plot(delays_us, p1_t1, "o", label="measured")
tt = np.linspace(0, delays_us.max(), 200)
plt.plot(tt, decay(tt, *popt), "-", label=rf"fit: $T_1$ = {T1_fit:.1f} µs")
plt.xlabel("delay τ (µs)")
plt.ylabel(r"$P_1$")
plt.title("$T_1$ relaxation")
plt.legend()
plt.tight_layout()
plt.show()
print(f"Measured T1 = {T1_fit:.1f} microseconds")
Measured T1 = 225.3 microseconds
Le temps exact varie d'une expérience à l'autre et d'un backend à l'autre, mais tu devrais obtenir un résultat d'environ quelques centaines de microsecondes.
Vérifie ta compréhension
En RMN quantitative, on te dit d'attendre environ entre deux scans. D'après la loi de décroissance, quelle fraction de l'aimantation d'équilibre est récupérée après ?
Réponse
Après , l'écart restant par rapport à l'équilibre est , c'est-à-dire qu'environ 99,3 % sont récupérés.
Expérience : ,
Théorie
mesure la perte d'énergie. mesure la perte de cohérence de phase dans le plan transversal. Nous pouvons l'observer avec une expérience de Ramsey : une impulsion fait basculer tous les spins à l'équateur, tous orientés dans la même direction. Ils précessent à la fréquence de Larmor, mais chacun précesse à une fréquence légèrement différente, si bien qu'ils s'étalent le long de l'équateur et que le signal transversal net décroît, avec un temps de décroissance caractéristique .
Remarque pour les lecteurs en informatique quantique : l'image de l'étalement montre une façon de perdre la cohérence : une dispersion de phases définies qui se moyennent. Il faut savoir que ce n'est pas le seul mécanisme. La cohérence peut aussi se perdre parce que la phase s'intrique avec l'environnement, de sorte qu'elle n'est plus du tout une valeur définie que le système possède. On ne peut pas distinguer les deux mécanismes en observant simplement la décroissance du signal ; l'image de l'étalement est donc une bonne intuition. Prends garde à ne pas la lire comme l'histoire complète ou littérale.
Deux effets déphasent un spin :
- Une décohérence irréversible (ou effectivement irréversible) provenant d'un couplage non contrôlé à d'autres degrés de liberté, caractérisée par le véritable . En RMN, elle vient généralement du mouvement moléculaire qui module les couplages entre spins. En informatique quantique, elle correspond aux couplages indésirables entre les qubits et l'environnement.
- Des décalages de fréquence réversibles, statiques ou lentement variables, comme l'inhomogénéité du champ, la variation de fréquence d'un qubit à l'autre et les erreurs de calibration. Combinés au véritable , ils produisent le temps de décroissance apparent plus court observé dans une expérience de Ramsey.
Alors, comment retrouver le véritable si une expérience de Ramsey ne nous renseigne que sur ? La seconde expérience, appelée écho de Hahn, annule tout le bruit du second type pour trouver le véritable . Elle commence comme une expérience de Ramsey, avec une impulsion , puis les spins s'étalent comme avant. Après un temps , une impulsion est appliquée. Elle inverse l'accumulation de phase causée par les décalages de fréquence statiques, de sorte que les spins se refocalisent après un second délai de même durée, .
et sont deux nombres importants à connaître, aussi bien en chimie qu'en informatique quantique. En chimie, fixe la largeur de raie d'une expérience RMN standard (tu verras ce que cela signifie dans une section ultérieure). peut en dire long sur les molécules, comme leur rotation moléculaire, et un changement de peut signaler qu'une molécule s'est liée à quelque chose de plus grand. En informatique quantique, nous voulons généralement que et soient aussi longs que possible, afin d'avoir plus de temps pour réaliser des expériences avant que la décohérence ne submerge le système. Toute une famille de techniques fondées sur l'écho de Hahn prolonge ce temps en annulant le bruit réversible, de sorte que le circuit soit idéalement limité par le véritable , et non par . C'est ce qu'on appelle le « découplage dynamique ».
Implémentation
Quand nous implémentons les expériences de Ramsey et d'écho de Hahn sur un qubit, la lecture est légèrement différente du cas de la RMN. En RMN, tu peux observer directement les champs oscillants induits par les spins qui précessent à la fréquence de Larmor dans le plan transversal. Mais avec un qubit, nous ne pouvons mesurer que sa projection selon l'axe , c'est-à-dire s'il est ou . Pour les deux expériences, nous terminons donc par une impulsion supplémentaire suivie d'une mesure. Cette impulsion traduit l'information de phase du qubit en une population.
Ci-dessous, nous exécutons chaque expérience pour déterminer et .
Ramsey () : deux impulsions avec un délai
La première impulsion bascule le vecteur de Bloch à l'équateur, et la suivante traduit la phase du vecteur de Bloch en population. Comme la phase change au cours du temps pendant la précession du vecteur de Bloch, nous voyons un motif sinusoïdal dans la population en fonction du temps de précession . À mesure que le qubit perd sa cohérence de phase, nous devrions voir l'amplitude de cette sinusoïde décroître.
Quelques subtilités de plus pour les lecteurs en informatique quantique : dans les expériences d'informatique quantique, les oscillations que nous voyons ne sont pas réellement à la fréquence de Larmor, qui est de l'ordre du GHz et beaucoup trop rapide pour être résolue. Les expériences quantiques se placent dans le référentiel tournant : un référentiel qui tourne avec le qubit à (presque) la fréquence de Larmor, de sorte que la précession rapide est invisible et que le vecteur de Bloch paraît presque immobile. Pour voir des franges, nous désaccordons volontairement un peu notre référentiel tournant par rapport à la fréquence de Larmor. Le vecteur de Bloch semble alors précesser à ce petit désaccord, et nous obtenons un signal oscillant à la fréquence de désaccord. Ci-dessous, nous ne fixons pas le désaccord à une valeur unique ; nous le choisissons de sorte qu'environ une douzaine de franges couvrent la fenêtre de mesure (que nous dimensionnons selon le temps de cohérence du qubit), ce qui permet d'échantillonner facilement l'oscillation sans repliement. Une autre complication : dans Qiskit, nous n'avons pas le contrôle au niveau des impulsions nécessaire pour changer la fréquence de l'impulsion (qui fixe la fréquence du référentiel tournant). À la place, nous ajoutons à la main un désaccord effectif, en l'utilisant pour fixer la phase de la seconde impulsion de Ramsey. C'est un peu artificiel, mais le résultat est le même et la décroissance que nous observons reste bien le véritable .
Une vue sur la sphère de Bloch
Une impulsion bascule les spins à l'équateur ; chacun précesse ensuite à une vitesse légèrement différente et ils s'étalent, de sorte que le vecteur net (rouge) rétrécit à mesure qu'ils se déphasent — cette décroissance est .
# Adaptive Ramsey: scale the window to the reported coherence and choose the detuning so a fixed
# number of fringes span it. This decouples the point count from the window -- a long window stays
# cheap and cannot alias (we keep 8 samples per fringe).
T2_us = (backend.qubit_properties(QUBIT).t2 or 100e-6) * 1e6 # reported (echo) T2 (us)
ramsey_span = 0.5 * T2_us # window = half the reported T2
n_fringes = 12 # fringes across the window
f_detune = n_fringes / (
ramsey_span * 1e-6
) # adaptive detuning (Hz) -> low, so a long window is cheap
samp_per_fringe = 8
n_ramsey = n_fringes * samp_per_fringe + 1 # ~97 points, fixed regardless of T2
ramsey_delays_us = np.linspace(0, ramsey_span, n_ramsey)
print(
f"Ramsey sweep: 0..{ramsey_span:.0f} us in {n_ramsey} points "
f"(detuning {f_detune/1e3:.1f} kHz, {samp_per_fringe} samples/fringe)"
)
dt = backend.dt # seconds per sample
delays_dt = [int(round(t * 1e-6 / dt)) for t in ramsey_delays_us] # integer samples
actual_delays_us = np.array(delays_dt) * dt * 1e6 # true (snapped) delays for fitting
ramsey_circ = []
for n_dt, tau_us in zip(delays_dt, actual_delays_us):
qc = QuantumCircuit(1, 1)
qc.rx(np.pi / 2, 0)
qc.delay(n_dt, 0, unit="dt") # integer duration -> no rounding warning
qc.rz(
2 * np.pi * f_detune * tau_us * 1e-6, 0
) # detuning phase (uses the true delay)
qc.rx(np.pi / 2, 0)
qc.measure(0, 0)
ramsey_circ.append(qc)
ramsey_isa = pm_q.run(ramsey_circ)
ramsey_res = sampler.run([(c,) for c in ramsey_isa], shots=1000).result()
p1_ramsey = np.array(
[r.data.c.get_counts().get("1", 0) / r.data.c.num_shots for r in ramsey_res]
)
ramsey_circ[len(ramsey_circ) // 2].draw("mpl") # draw a sample circuit (mid-sweep)
Ramsey sweep: 0..248 us in 97 points (detuning 48.4 kHz, 8 samples/fringe)
Écho de Hahn () : ajouter une impulsion de refocalisation au milieu
Une vue sur la sphère de Bloch
L'écho de Hahn : les spins s'étalent comme avant, puis une impulsion les retourne, de sorte que leur précession continue les refocalise ensemble (le vecteur rouge regrandit), récupérant la cohérence perdue à cause du déphasage statique.
# Scale the echo sweep to the reported (echo) T2. The echo signal is monotonic (no fringes),
# so only the range matters here; 0 .. ~2.5 T2 shows the full decay for a clean fit.
T2_us = (backend.qubit_properties(QUBIT).t2 or 100e-6) * 1e6
echo_delays_us = np.linspace(0, 2.5 * T2_us, 24) # total free-evolution time
print(f"Echo sweep: 0..{2.5*T2_us:.0f} us in {len(echo_delays_us)} points")
dt = backend.dt
half_dt = [
int(round(t / 2 * 1e-6 / dt)) for t in echo_delays_us
] # samples per half-delay
actual_echo_us = np.array(half_dt) * 2 * dt * 1e6 # true total delay for fitting
echo_circ = []
for n_half in half_dt:
qc = QuantumCircuit(1, 1)
qc.rx(np.pi / 2, 0)
qc.delay(n_half, 0, unit="dt") # half the free-evolution, integer samples
qc.rx(np.pi, 0) # refocusing pi pulse
qc.delay(n_half, 0, unit="dt")
qc.rx(-np.pi / 2, 0)
qc.measure(0, 0)
echo_circ.append(qc)
echo_isa = pm_q.run(echo_circ)
echo_res = sampler.run([(c,) for c in echo_isa], shots=1000).result()
p1_echo = np.array(
[r.data.c.get_counts().get("1", 0) / r.data.c.num_shots for r in echo_res]
)
echo_circ[len(echo_circ) // 2].draw("mpl") # draw a sample circuit
Echo sweep: 0..1240 us in 24 points
Analyser : ajuster et comparer les deux décroissances
# Initial guesses / bounds derived from the reported coherence so the fits track the scaled axes.
T2_us = (backend.qubit_properties(QUBIT).t2 or 100e-6) * 1e6
# Ramsey: decaying oscillation -> extract T2*
def ramsey_fn(t, A, T2s, f, phi, B):
return A * np.exp(-t / T2s) * np.cos(2 * np.pi * f * t + phi) + B
rp0 = [0.4, T2_us / 2, f_detune * 1e-6, 0.0, 0.5] # T2* guess ~ half the echo T2
rbounds = (
[0, 0.1, 0, -np.pi, 0],
[1, 2 * T2_us, 5, np.pi, 1],
) # keep the fit from collapsing to ~0
rpopt, _ = curve_fit(
ramsey_fn, ramsey_delays_us, p1_ramsey, p0=rp0, bounds=rbounds, maxfev=20000
)
T2_star = abs(rpopt[1])
# Echo: simple exponential envelope -> extract T2
def env(t, A, T2, B):
return A * np.exp(-t / T2) + B
epopt, _ = curve_fit(env, echo_delays_us, p1_echo, p0=[0.5, T2_us, 0.0], maxfev=10000)
T2 = abs(epopt[1])
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].plot(ramsey_delays_us, p1_ramsey, "o")
tt = np.linspace(0, ramsey_delays_us.max(), 400)
ax[0].plot(tt, ramsey_fn(tt, *rpopt), "-")
ax[0].set_title(rf"Ramsey: $T_2^*$ = {T2_star:.1f} µs")
ax[0].set_xlabel("τ (µs)")
ax[0].set_ylabel(r"$P_1$")
ax[1].plot(echo_delays_us, p1_echo, "o")
tt2 = np.linspace(0, echo_delays_us.max(), 400)
ax[1].plot(tt2, env(tt2, *epopt), "-")
ax[1].set_title(rf"Hahn echo: $T_2$ = {T2:.1f} µs")
ax[1].set_xlabel("τ (µs)")
plt.tight_layout()
plt.show()
print(f"T2* (Ramsey) = {T2_star:.1f} µs ; T2 (echo) = {T2:.1f} µs")

T2* (Ramsey) = 171.2 µs ; T2 (echo) = 326.0 µs
Nous voyons ici à la fois l'oscillation de Ramsey et la décroissance du signal d'écho de Hahn, comme attendu. Les échelles de temps associées à ces décroissances sont respectivement et .
Vérifie ta compréhension
Tu devrais typiquement trouver . Explique pourquoi en une phrase, et dis quel effet physique l'impulsion de l'écho de Hahn supprime, ainsi que ce qu'elle ne peut pas supprimer.
Réponse
inclut à la fois le déphasage irréversible et la dispersion statique réversible, il décroît donc plus vite ; l'impulsion de l'écho refocalise le déphasage statique et réversible (inhomogénéité du champ / décalages de fréquence fixes), ne laissant que le déphasage spin-spin irréversible, qui est le véritable . L'écho ne peut pas défaire les fluctuations qui changent entre les deux moitiés de la séquence.
Spectroscopie RMN d'une molécule : déplacement chimique et couplage
D'un spin à une molécule
Les expériences jusqu'ici caractérisaient un seul spin isolé. La RMN chimique réelle lit une molécule (de nombreux spins couplés à la fois) et utilise le spectre obtenu pour en déduire des caractéristiques de la structure de la molécule. Cela peut nous renseigner sur deux effets :
-
Déplacement chimique. Chaque noyau chimiquement distinct précesse à une fréquence légèrement différente, parce que ses électrons locaux blindent le champ appliqué de façon différente. Les positions des pics identifient donc les environnements chimiques (un proton CH par rapport à un proton aromatique, par exemple).
-
Couplage scalaire . Une interaction à travers les liaisons entre deux spins scinde chaque pic en un multiplet. Un noyau ayant voisins équivalents est scindé en raies d'intensités binomiales, la règle des . Le motif de scission révèle la connectivité (quels noyaux sont liés), et l'espacement est la constante de couplage .
L'expérience
Pour modéliser cela sur notre ordinateur quantique, nous devons implémenter un hamiltonien de spin sur les qubits. Dans la limite de couplage faible et de champ fort (différences de déplacement chimique couplages), les parties et du couplage se moyennent à zéro, ne laissant que . L'hamiltonien est donc (en unités de pulsation, ) :
Chaque noyau est un qubit : le déplacement chimique est une rotation à un qubit, et le couplage est une interaction à deux qubits.
L'expérience se déroule ainsi. Une impulsion bascule chaque spin dans le plan transversal, et les spins précessent et se couplent pendant un temps . Nous enregistrons l'aimantation transversale complexe sommée sur tous les spins. Comme dans l'expérience de Ramsey ci-dessus, cette aimantation transversale dessine une sinusoïde décroissante en fonction du temps de précession . Quand nous prenons la transformée de Fourier du signal, nous obtenons un spectre avec des pics au déplacement chimique scindés par le couplage scalaire .
Nous simulons l'évolution de la molécule en associant le temps à des angles de rotation : un plus grand signifie des rotations et plus grandes, et non un circuit plus long ou plus profond. Ainsi, bien que l'état simulé avance avec (c'est ce qui fait osciller la décroissance libre d'induction (FID)), les qubits physiques n'attendent jamais plus longtemps et il n'y a pas de décohérence dépendant de . La FID simulée ne décroît donc jamais d'elle-même. Pour une FID idéale infiniment longue, les raies spectrales seraient infiniment fines ; notre fenêtre d'échantillonnage finie leur donne à la place une résolution finie et peut introduire une fuite spectrale. Les raies RMN réelles ont une largeur finie fixée par . Nous la rajoutons en multipliant la FID par une fenêtre avant la transformée de Fourier. C'est ce qu'on appelle une apodisation d'élargissement de raie, que les spectroscopistes appliquent lors du traitement pour fixer la largeur de raie (en échangeant de la résolution contre du rapport signal sur bruit). (Une cellule optionnelle dans l'annexe de ce module présente une alternative trotterisée, où la décroissance provient plutôt d'une vraie décohérence qui croît avec la profondeur du circuit.)
Le système AX
Commençons par ce qu'on appelle le système AX, une molécule constituée de deux spins couplés et chimiquement distincts. Les étiquettes A et X désignent les deux environnements de spin chimiquement distincts. Les lettres sont arbitraires, mais le fait qu'elles soient éloignées dans l'alphabet illustre leur dissemblance chimique.
Associer et optimiser
Nous écrivons une fonction qui, à partir des décalages de déplacement chimique et des couplages , renvoie un circuit de préparation d'état paramétré. L'impulsion bascule les spins à l'équateur, puis les rotations simulent l'évolution de l'état sous les termes de déplacement chimique et de couplage . Chaque est un petit décalage de précession en Hz par rapport à une référence choisie, ou porteuse du référentiel tournant. Ici, nous le fixons manuellement ; dans une vraie molécule, le blindage électronique modifie le champ magnétique local et décale la résonance de chaque noyau par rapport à cette référence. Cela est modélisé par les portes à un qubit, une par spin, et chaque fixe l'endroit où se situe le pic de ce spin dans le spectre. La porte à deux qubits applique le couplage , qui scinde chaque pic en un multiplet et fixe l'espacement entre ses raies.
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
t = Parameter(
"t"
) # evolution time (arbitrary units; the frequencies below are in matching units)
def fid_circuit(offsets, couplings):
"""State-preparation circuit for a set of spins: a pi/2 pulse, then evolution under the
chemical-shift + J-coupling Hamiltonian for time t. No measurement -- the Estimator reads
<X> and <Y> directly.
offsets : list of chemical-shift frequencies nu_i, one per qubit
couplings : dict {(i, j): J_ij}
"""
n = len(offsets)
qc = QuantumCircuit(n)
qc.ry(np.pi / 2, range(n)) # pi/2 pulse: tip every spin into the transverse plane
for i, nu in enumerate(offsets):
qc.rz(2 * np.pi * nu * t, i) # chemical-shift precession -> line at nu_i
for (i, j), J in couplings.items():
qc.rzz(np.pi * J * t, i, j) # J-coupling -> splitting of size J
return qc
def transverse_obs(n):
"""Total transverse observables (sum over spins): (sum_i X_i, sum_i Y_i)."""
ox = SparsePauliOp.from_sparse_list(
[("X", [i], 1.0) for i in range(n)], num_qubits=n
)
oy = SparsePauliOp.from_sparse_list(
[("Y", [i], 1.0) for i in range(n)], num_qubits=n
)
return ox, oy
# AX system: two distinct chemical shifts, one coupling
ax_offsets = [6.0, -6.0] # chemical shifts nu_1, nu_2 (Hz)
ax_couplings = {(0, 1): 2.0} # J_12 = 2 Hz
ax_isa = pm_ax.run(fid_circuit(ax_offsets, ax_couplings))
fid_circuit(ax_offsets, ax_couplings).draw("mpl")
Exécuter et post-traiter
Nous évaluons le circuit sur une grille de temps d'évolution, et l'Estimator renvoie et pour chacun. Nous les combinons en la FID complexe , appliquons la fenêtre d'apodisation et effectuons la transformée de Fourier du résultat pour obtenir le spectre.
# --- time grid ---
n_t = 128 # number of FID samples
dt = 1 / 32 # time step -> Nyquist frequency = 1/(2 dt) = 16 Hz
times = np.arange(n_t) * dt
# --- Step 3-4: Estimator returns <X> and <Y> directly (both quadratures in one job) ---
def run_fid(isa_circ, n, est):
"""Complex FID s(t) = sum_i <X_i> + i <Y_i> over the time grid, via the Estimator."""
ox, oy = transverse_obs(n)
ox = ox.apply_layout(isa_circ.layout) # map observables onto the transpiled qubits
oy = oy.apply_layout(isa_circ.layout)
params = times.reshape(-1, 1)
res = est.run(
[(isa_circ, ox, params), (isa_circ, oy, params)]
).result() # both quadratures, one job
return res[0].data.evs + 1j * res[1].data.evs
def spectrum(fid, T2=1.0):
sig = fid * np.exp(-times / T2) # apodization -> Lorentzian linewidth ~ 1/(pi T2)
spec = np.fft.fftshift(np.fft.fft(sig))
freq = np.fft.fftshift(np.fft.fftfreq(len(times), dt))
return freq, np.abs(spec)
fid_ax = run_fid(ax_isa, n=2, est=estimator)
freq, spec = spectrum(fid_ax, T2=1.0)
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].plot(times, fid_ax.real, label=r"Re $s(t)=\langle X\rangle$")
ax[0].plot(times, fid_ax.imag, label=r"Im $s(t)=\langle Y\rangle$")
ax[0].set_xlabel("evolution time $t$")
ax[0].set_ylabel("transverse signal")
ax[0].set_title("AX free induction decay")
ax[0].legend()
ax[1].plot(freq, spec)
for nu in ax_offsets:
for s in (-1, 1):
ax[1].axvline(nu + s * ax_couplings[(0, 1)] / 2, color="gray", ls="--", lw=1)
ax[1].set_xlim(-12, 12)
ax[1].set_xlabel(r"frequency $\nu$ (Hz)")
ax[1].set_ylabel("intensity")
ax[1].set_title("AX spectrum: two doublets, split by $J$")
plt.tight_layout()
plt.show()
print(
"Expected AX lines (Hz):",
sorted({nu + s * ax_couplings[(0, 1)] / 2 for nu in ax_offsets for s in (-1, 1)}),
)

Expected AX lines (Hz): [-7.0, -5.0, 5.0, 7.0]
Extension : un système AX et la règle des
Ajoutons maintenant un second voisin équivalent. Dans un système AX, un spin A se couple, avec le même , à deux spins X équivalents. (Rappelle-toi que X n'est qu'une étiquette pour les deux spins équivalents ; elle n'a rien à voir avec les opérateurs de Pauli . Tous les couplages ci-dessous restent .) Le spin A voit alors quatre configurations de voisins ; les deux configurations mixtes coïncident, donc A est scindé en un triplet 1:2:1 ( raies). Chaque spin X ne voit que A et reste un doublet. Les spins équivalents ne se scindent pas entre eux, nous n'incluons donc aucun couplage X–X.
Une vraie molécule présentant ce motif est le 1,1,2-trichloroéthane (ClCH–CHCl). Lorsqu'on cible les spins de l'hydrogène (la forme de RMN la plus courante), l'unique proton d'hydrogène du groupe CH joue le rôle de A, et les deux protons équivalents de CH sont les deux spins X. Il suffit maintenant de compter les voisins : le proton CH a deux voisins (la paire CH), son signal est donc scindé en un triplet 1:2:1 () ; chaque proton CH n'a qu'un voisin (le CH), le signal de CH est donc un doublet 1:1 (). Remarque que le groupe ayant moins de protons (CH) montre plus de raies ; le nombre de raies reflète combien de voisins a un signal, et non combien de protons le partagent.
Ci-dessous, nous simulons un système AX avec trois qubits.
# AX2: spin A (qubit 0) coupled equally to two equivalent X spins (qubits 1, 2)
ax2_offsets = [6.0, -6.0, -6.0] # A at +6 Hz; the two X both at -6 Hz
ax2_couplings = {
(0, 1): 2.0,
(0, 2): 2.0,
} # equal A-X couplings; no coupling between the X spins
ax2_isa = pm_ax2.run(fid_circuit(ax2_offsets, ax2_couplings))
fid_ax2 = run_fid(ax2_isa, n=3, est=estimator)
freq2, spec2 = spectrum(fid_ax2, T2=1.0)
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].plot(times, fid_ax2.real, label=r"Re $s(t)=\langle X\rangle$")
ax[0].plot(times, fid_ax2.imag, label=r"Im $s(t)=\langle Y\rangle$")
ax[0].set_xlabel("evolution time $t$")
ax[0].set_ylabel("transverse signal")
ax[0].set_title("AX$_2$ free induction decay")
ax[0].legend()
ax[1].plot(freq2, spec2)
ax[1].set_xlim(-12, 12)
ax[1].set_xlabel(r"frequency $\nu$ (Hz)")
ax[1].set_ylabel("intensity")
ax[1].set_title(r"AX$_2$ spectrum: A is a 1:2:1 triplet, X is a doublet")
plt.tight_layout()
plt.show()
print(
"A triplet at (Hz):",
[ax2_offsets[0] + k * ax2_couplings[(0, 1)] for k in (-1, 0, 1)],
)
print(
"X doublet at (Hz):",
[ax2_offsets[1] + s * ax2_couplings[(0, 1)] / 2 for s in (-1, 1)],
)
Vérifie ta compréhension
Lis chaque question, réfléchis à ta réponse, puis clique sur le triangle pour révéler la solution.
Dans le spectre AX, les deux doublets sont séparés (centre à centre) de 12 Hz, et les deux raies de chaque doublet sont distantes de 2 Hz. Quel nombre est la différence de déplacement chimique et lequel est la constante de couplage ? Si tu passais à un spectromètre avec un champ magnétique deux fois plus fort, lequel changerait ?
Réponse
La séparation de 12 Hz centre à centre est la différence de déplacement chimique () ; l'espacement de 2 Hz entre raies est la constante de couplage . Les déplacements chimiques proviennent du blindage de , ils varient donc avec le champ. Avec un champ deux fois plus fort, la séparation double et passe à 24 Hz. Le couplage est une propriété moléculaire à travers les liaisons et est indépendant du champ, il reste donc à 2 Hz. Les chimistes distinguent le déplacement du couplage en observant comment une scission varie avec le champ.
En utilisant la règle des , prédis le multiplet de A si l'on ajoutait un troisième spin X équivalent (un système AX), et explique pourquoi les deux spins X équivalents de AX ne se scindent pas mutuellement.
Réponse
Avec voisins équivalents, A devient un quadruplet 1:3:3:1. Les spins équivalents ne se scindent pas entre eux parce que leur couplage décale tous les niveaux d'énergie concernés de la même quantité, de sorte que les écarts entre ces niveaux — les fréquences que nous voyons — ne changent pas.
Conclusion
Nous avons utilisé un vrai ordinateur quantique pour explorer comment les spins nucléaires précessent et se décohèrent dans une expérience de RMN, et comment interpréter les spectres obtenus. Il vaut la peine de prendre du recul pour revoir pourquoi cela était possible, et pourquoi c'est plus qu'une simple curiosité.
Un qubit et un noyau de spin 1/2 sont, mathématiquement, le même objet : des systèmes quantiques à deux niveaux vivant sur la sphère de Bloch, mis en rotation par des impulsions résonantes, précessant autour d'un axe et perdant leur cohérence au contact de leur environnement. Tout le vocabulaire de ce module (la fréquence de Larmor, le référentiel tournant, , et , les franges de Ramsey, l'écho de Hahn) appartient autant à la RMN qu'à l'informatique quantique. Ce qu'un chimiste appelle un déplacement chimique, un ingénieur quantique l'appelle une rotation à un qubit ; ce qu'un chimiste appelle un couplage , un ingénieur quantique l'appelle une interaction . Les deux domaines décrivent la même physique avec des mots différents.
Ce lien est historique, pas seulement conceptuel : la RMN a été l'une des premières plateformes à exécuter de vrais algorithmes quantiques. En 2001, une molécule en solution avait exécuté l'algorithme de Shor pour factoriser 15, chaque spin nucléaire servant de qubit. La RMN a aussi légué à l'informatique quantique une grande partie de sa boîte à outils de contrôle. L'écho de spin, les trains d'impulsions CPMG, les impulsions composites et le découplage dynamique ont tous été inventés par des spectroscopistes des décennies avant que les qubits n'en aient besoin pour lutter contre la décohérence. Il y a une symétrie satisfaisante dans ce que nous venons de faire : le domaine qui a donné naissance aux premiers ordinateurs quantiques est maintenant quelque chose qu'un ordinateur quantique peut simuler.
Révision et questions
Concepts essentiels
-
Un qubit est un système à deux niveaux conçu, équivalent à un spin 1/2 ; ses , sa fréquence, ses portes, et sont les analogues directs des grandeurs de la RMN.
-
Une impulsion résonante fait tourner le vecteur de Bloch ; l'angle de bascule fixe la rotation, et la courbe de Rabi/nutation identifie les rotations et .
-
est la relaxation de l'énergie (spin-réseau) ; est la relaxation de la phase (spin-spin) ; inclut en plus le déphasage statique réversible qu'un écho de Hahn refocalise.
-
Une molécule est un ensemble de spins couplés : le déplacement chimique fixe la position de chaque pic, et le couplage scinde les pics en multiplets selon la règle des , ce qui encode ensemble l'environnement chimique et la connectivité.
Vrai / faux
-
V/F La fréquence de Larmor est l'écart d'énergie entre les deux états de spin divisé par .
-
V/F Une impulsion de () inverse complètement un spin à l'équilibre.
-
V/F est généralement plus long que .
-
V/F L'écho de Hahn refocalise le déphasage spin-spin irréversible.
-
V/F Dans un système AX, le noyau A apparaît comme un triplet 1:2:1.
-
V/F La constante de couplage double quand tu doubles le champ magnétique du spectromètre.
-
V/F Supprimer Qiskit Pulse rend impossible la mesure de sur le matériel actuel.
Réponse courte
-
Explique en deux phrases pourquoi une seule impulsion est un mauvais moyen de détecter une erreur d'angle de bascule de , et comment l'enchaînement d'impulsions résout ce problème.
-
Un échantillon présente de larges raies RMN. En termes de , que cela t'indique-t-il, et quelle grandeur du qubit en est l'analogue ?
-
Donne l'analogue pour le qubit de chacun : (a) le délai de récupération, (b) l'impulsion de , (c) le déplacement chimique.
-
En utilisant la règle des , explique pourquoi un groupe éthyle (CHCH–) présente un triplet 1:2:1 et un quadruplet 1:3:3:1, et identifie quel groupe produit quel multiplet.
Problèmes de défi
-
Train de Carr-Purcell. Modifie l'écho de Hahn pour insérer impulsions régulièrement espacées (un train de type CPMG) et mesure en fonction de . Discute pourquoi davantage d'impulsions de refocalisation peuvent allonger le apparent.
-
Amplification de l'erreur d'impulsion. Construis un circuit qui applique fois à et trace en fonction de pour un angle volontairement mal réglé . Montre comment l'erreur devient visible à mesure que augmente. Indice : place une barrière après chaque rotation ; sinon le transpiler peut combiner les rotations répétées en une seule porte équivalente.
-
Groupe éthyle. En utilisant
fid_circuit, construis le spectre du proton d'un fragment éthyle (CH–CH) : cinq qubits, les trois protons CH partageant un même déplacement chimique et les deux protons CH un autre, et chaque paire CH–CH couplée par le même (pas de couplage au sein d'un groupe équivalent). Prédis le motif avec la règle des , puis simule-le ; tu devrais retrouver un quadruplet 1:3:3:1 (CH) et un triplet 1:2:1 (CH).
Annexe (optionnelle) : évolution trotterisée — laisser la décohérence fixer la largeur de raie
Dans la section Spectroscopie RMN d'une molécule, l'évolution est appliquée en une seule couche dont la profondeur ne croît pas avec , de sorte que la FID ne décroît pas d'elle-même et nous imposons la largeur de raie avec la fenêtre d'apodisation . Cette cellule optionnelle adopte l'approche opposée. Elle découpe l'évolution en pas de Trotter et place une barrière après chaque pas (pour que le transpiler ne puisse pas les refusionner en une seule couche). Le point temporel utilise pas, de sorte que la profondeur du circuit croît avec le temps.
Comme tous les termes de commutent, c'est exact : sur un simulateur sans bruit, cela donne les mêmes pics fins. Mais sur un backend à modèle de bruit ou du vrai matériel, l'erreur de porte qui s'accumule fait décroître la FID d'elle-même, et c'est cette décroissance qui fixe la largeur de raie. C'est l'idée largeur de raie de la section de l'expérience , , qui provient maintenant d'une vraie décohérence plutôt que d'une fenêtre de traitement. Nous exécutons de cette façon à la fois le système AX (deux doublets) et le système AX (un triplet 1:2:1 plus un doublet).
Deux remarques pratiques à prendre en compte. D'abord, le coût est la profondeur : le circuit AX le plus profond utilise portes à deux qubits et la version AX environ le double, les deux sont donc bien plus bruités et coûteux que la version exacte. Ensuite, chaque système de spins est toujours soumis comme un seul job, et chaque point temporel réutilise la même disposition de qubits physiques sélectionnée dans la section de spectroscopie. Si USE_BATCH=True, ces jobs utilisent aussi le Batch optionnel du notebook entier.
# Trotterized FID: depth grows with time, so real decoherence (not apodization) sets the linewidth.
estimator.options.default_shots = 2000 # deep circuits need more shots in this appendix
n_tr, dt_tr = (
48,
1 / 20,
) # Nyquist = 1/(2 dt) = 10 Hz (covers the AX2 triplet's 8 Hz line)
times_tr = np.arange(n_tr) * dt_tr
APODIZE_TROTTER = False # keep False so the linewidth comes from decoherence
def trotter_fid_circuit(offsets, couplings, p, step):
n = len(offsets)
qc = QuantumCircuit(n)
qc.ry(np.pi / 2, range(n)) # 90-degree pulse
for _ in range(p): # p Trotter steps, each of duration `step`
for i, nu in enumerate(offsets):
qc.rz(2 * np.pi * nu * step, i) # chemical-shift evolution for one step
for (i, j), J in couplings.items():
qc.rzz(np.pi * J * step, i, j) # J-coupling evolution for one step
qc.barrier() # <-- stops the transpiler merging the steps
return qc
def run_trotter(offsets, couplings, n, est, pass_manager):
# Time point k uses k Trotter steps (its own circuit). Submit every time point and both
# quadratures together as ONE Estimator job, using one fixed physical-qubit layout.
ox, oy = transverse_obs(n)
pubs = []
for k in range(n_tr):
isa = pass_manager.run(trotter_fid_circuit(offsets, couplings, k, dt_tr))
pubs.append((isa, ox.apply_layout(isa.layout))) # <X> at time k
pubs.append((isa, oy.apply_layout(isa.layout))) # <Y> at time k
res = est.run(pubs).result()
return np.array(
[res[2 * k].data.evs + 1j * res[2 * k + 1].data.evs for k in range(n_tr)]
)
def trotter_spectrum(fid):
sig = fid * np.exp(-times_tr / 1.0) if APODIZE_TROTTER else fid
spec = np.abs(np.fft.fftshift(np.fft.fft(sig)))
freq = np.fft.fftshift(np.fft.fftfreq(n_tr, dt_tr))
return freq, spec
fid_tr = run_trotter(
ax_offsets, ax_couplings, 2, estimator, pm_ax
) # AX -> two doublets
fid2_tr = run_trotter(
ax2_offsets, ax2_couplings, 3, estimator, pm_ax2
) # AX2 -> triplet + doublet
f_ax, s_ax = trotter_spectrum(fid_tr)
f_ax2, s_ax2 = trotter_spectrum(fid2_tr)
fig, ax = plt.subplots(1, 3, figsize=(15, 4))
ax[0].plot(times_tr, np.abs(fid_tr), "o-", label="AX")
ax[0].plot(times_tr, np.abs(fid2_tr), "s-", label="AX$_2$")
ax[0].set_xlabel("evolution time $t$")
ax[0].set_ylabel("|FID|")
ax[0].set_title("Trotter FID envelope (decays only on a noisy backend)")
ax[0].legend()
ax[1].plot(f_ax, s_ax)
ax[1].set_xlim(-12, 12)
ax[1].set_xlabel(r"frequency $\nu$ (Hz)")
ax[1].set_ylabel("intensity")
ax[1].set_title("AX: two doublets")
ax[2].plot(f_ax2, s_ax2)
ax[2].set_xlim(-12, 12)
ax[2].set_xlabel(r"frequency $\nu$ (Hz)")
ax[2].set_title("AX$_2$: 1:2:1 triplet + doublet")
plt.tight_layout()
plt.show()
print(
f"Deepest AX circuit: {n_tr-1} two-qubit gates; deepest AX2 circuit: {2*(n_tr-1)} two-qubit gates."
)
print(
"Noiseless backend: |FID| stays flat, peaks razor-sharp (set APODIZE_TROTTER=True to tame ripples)."
)
print(
"Noise-model backend / hardware: |FID| decays on its own and the peaks gain a real linewidth."
)
# Close the optional runtime Batch, if one was opened.
if batch is not None:
batch.close()