Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Kernspinresonanz mit Qiskit

Für dieses Qiskit-in-Classrooms-Modul benötigen Studierende eine funktionierende Python-Umgebung, in der die folgenden Pakete installiert sind:

  • qiskit v2.1 oder neuer
  • qiskit-ibm-runtime v0.40.1 oder neuer
  • qiskit-aer v0.17 oder neuer
  • scipy und matplotlib (zum Fitten und Plotten)
  • pylatexenc (zum Rendern von Circuit-Diagrammen)

Informationen zum Einrichten und Installieren der oben genannten Pakete findest du im Leitfaden Qiskit installieren. Um Jobs auf echten Quantencomputern auszuführen, müssen Studierende ein Konto bei IBM Quantum® einrichten, indem sie die Schritte im Leitfaden Dein IBM Cloud®-Konto einrichten befolgen.

Dieses Modul wurde getestet und verbraucht etwa 5 Minuten QPU-Zeit. Dies ist eine Schätzung, und deine tatsächliche Nutzung kann abweichen.

Einführung​

Wenn ein Chemiker ein NMR-Experiment durchführt, bringt er eine Probe von Kernspins in ein starkes Magnetfeld, wendet einen Radiofrequenz-Puls (RF-Puls) an und misst das zurückkommende Signal. Die Technik beruht auf einigen Ideen aus der Quantenmechanik eines Spin-1/2-Teilchens in einem Magnetfeld. Der Spin hat zwei Energieniveaus, getrennt durch eine Energie, die einer charakteristischen Larmor-Frequenz entspricht. Ein resonanter Puls bei dieser Frequenz kippt den Spin in die transversale Ebene, wo er präzediert und ein Signal induziert, das anschließend mit zwei charakteristischen Zeiten abklingt, T1T_1 und T2T_2.

Ein supraleitendes Qubit ist eine technisch hergestellte Version desselben Objekts: ein kontrollierbares quantenmechanisches Zwei-Niveau-System. Seine Zustände ∣0⟩|0\rangle und ∣1⟩|1\rangle sind das Analogon zu Spin-up und Spin-down, das Gate, mit dem wir es drehen, ist das Analogon zum RF-Puls, und es verliert Kohärenz mit eigenen T1T_1 und T2T_2. Das ist nicht bloß ein loser Vergleich; einige der frühesten experimentellen Quantenprozessoren Ende der 1990er und Anfang der 2000er Jahre nutzten NMR.

In diesem Modul verwendest du einen echten Quantencomputer, um dieselben Charakterisierungsexperimente durchzuführen, die ein NMR-Spektroskopiker (oder ein Quanteningenieur bei der Kalibrierung von Hardware) ausführt:

  • Rabi / Nutation — den Spin antreiben und beobachten, wie er rotiert. So kalibrierst du eine π\pi- oder π/2\pi/2-Rotation.

  • T1T_1 — Spin-Gitter-Relaxation, das Zurückfallen eines angeregten Spins ins Gleichgewicht.

  • T2T_2 — Spin-Spin-Relaxation (Dephasierung), gemessen mit einem Ramsey-Experiment und refokussiert mit einem Hahn-Echo.

  • NMR-Spektroskopie (chemische Verschiebung und JJ-Kopplung) — ein kleines Molekül auf mehreren Qubits simulieren und sein Multiplett-Spektrum auslesen, um die Konnektivität über die n+1n+1-Regel zu ermitteln.

Für jedes Konzept geben wir zunächst die minimale Theorie an und implementieren das Experiment dann in Qiskit nach dem vierstufigen Workflow der Qiskit patterns.

Theorie​

Spin-1/2-Teilchen in einem Magnetfeld​

Ein Spin-1/2-Kern hat ein magnetisches Moment μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}, wobei γ\gamma das gyromagnetische Verhältnis ist. In einem statischen Feld B0B_0 entlang zz spalten sich die beiden Spinzustände energetisch auf (der Zeeman-Effekt):

E±=∓12γℏB0,ΔE=γℏB0=ℏω0.E_{\pm} = \mp \tfrac{1}{2}\gamma \hbar B_0, \qquad \Delta E = \gamma \hbar B_0 = \hbar \omega_0 .

Die Energielücke definiert die Larmor-(Resonanz-)Frequenz ω0=γB0\omega_0 = \gamma B_0. Das magnetische Moment präzediert mit genau dieser Frequenz um B0B_0.

Um den Spin zu drehen, wenden wir ein oszillierendes RF-Feld bei (oder nahe) ω0\omega_0 an. Im mit der Treiberfrequenz rotierenden Bezugssystem (dem rotierenden Bezugssystem) dreht ein resonanter Antrieb den Bloch-Vektor um eine Achse in der xx-yy-Ebene. Wie weit er sich dreht (der Flipwinkel), wird durch Stärke und Dauer des Pulses festgelegt. Zwei Wahlen sind am wichtigsten: Ein 90∘90^\circ-Puls kippt einen +1/2+1/2-Spin in die transversale Ebene, und ein 180∘180^\circ-Puls dreht ihn vollständig nach −1/2-1/2.

Zwischen NMR- und Quantencomputing-Jargon übersetzen​

Ein Qubit ist ein Zwei-Niveau-System, mathematisch identisch mit einem Spin-1/2 in einem Magnetfeld — obwohl wir also auf einem Quantenprozessor statt auf einem Atomkern arbeiten, ist die Physik dieselbe, die auch NMR zugrunde liegt. Nur das Vokabular unterscheidet sich. Diese Tabelle hilft dir, mit Physikochemikern und Quantencomputing-Spezialisten gleichermaßen zu sprechen.

NMR / Spin-1/2Quantenprozessor (Qubit)
Spin-up / Spin-down∣0⟩\ket{0} / ∣1⟩\ket{1}
Zeeman-Lücke ΔE=ℏω0\Delta E = \hbar\omega_0Qubit-Übergangsenergie ℏω01\hbar\omega_{01}
Larmor-Frequenz ω0\omega_0Qubit-Frequenz ω01\omega_{01}
Resonanter RF-Puls (Flipwinkel θ\theta)Ein-Qubit-Rotations-Gate RX(θ)R_X(\theta)
90∘90^\circ- / 180∘180^\circ-PulsRX(π/2)R_X(\pi/2) / RX(π)R_X(\pi)
Spin-Gitter-Relaxation T1T_1Energierelaxationszeit des Qubits T1T_1
Spin-Spin-Relaxation T2T_2Dephasierungszeit des Qubits T2T_2
NutationRabi-Oszillationen
Freier Induktionszerfall (FID), Hahn-EchoRamsey-, Echo-Sequenzen

Alles Folgende ist eine Übung darin, die rechte Spalte zu messen und sie als linke Spalte zu lesen.

Die Qiskit-Umgebung einrichten​

Wir folgen durchgehend dem Workflow der Qiskit patterns:

  1. Das Problem auf einen Quantencircuit abbilden (Map).

  2. Den Circuit für die Ausführung optimieren (Optimize; auf das Backend transpilieren).

  3. Mit einem IBM-Quantum-Primitiv ausführen (Execute).

  4. Nachverarbeiten (Post-process) und analysieren.

Zuerst laden wir den Runtime-Service und wählen das am wenigsten ausgelastete echte Backend. Wenn du deine Zugangsdaten noch nicht gespeichert hast, findest du auf der Dokumentationsseite Zugangsdaten speichern eine Anleitung.

# 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

Verwende die Zelle unten, um für das gesamte Notebook zwischen echter Hardware und einem lokalen Simulator umzuschalten. Die Ausführung auf Hardware ist der Kern des Moduls, aber bei Konnektivitäts- oder Nutzungsproblemen kann der Simulator als Ausweichlösung dienen. Setze USE_HARDWARE = True, um auf der oben ausgewählten QPU auszuführen.

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}.")

Experiment: Rabi-Oszillationen (Nutation)​

Theorie​

Treibst du einen Spin auf Resonanz an, rotiert sein Bloch-Vektor gleichmäßig von Pol zu Pol — er nutiert. Wenden wir auf ein Qubit, das in ∣0⟩|0\rangle startet, eine Rotation mit dem Flipwinkel θ\theta um die xx-Achse an, so beträgt die Wahrscheinlichkeit, es in ∣1⟩|1\rangle zu finden:

P1(θ)=sin⁡2 ⁣(θ2)=12(1−cos⁡θ).P_1(\theta) = \sin^2\!\left(\tfrac{\theta}{2}\right) = \tfrac{1}{2}\bigl(1 - \cos\theta\bigr).

Das ist die Rabi-Kurve. Ihr erstes Maximum ist der π\pi-Puls (vollständige Inversion, der 180∘180^\circ-Puls); ein π/2\pi/2-Puls ist eine Vierteldrehung (der 90∘90^\circ-Puls, der einen Spin in die transversale Ebene kippt). In der NMR kalibrierst du mit dieser Kurve deine Pulse.

Experimenteller Hinweis

Ein wörtliches Rabi-Experiment variiert die Antriebsamplitude oder -dauer in der Zeit. Das erfordert Steuerung auf Pulsebene, die in Qiskit v2.0 entfernt wurde. Hier variieren wir stattdessen den Rotationswinkel θ\theta eines RX(θ)R_X(\theta)-Gates. Die Kurve und die Zuordnung von π\pi/π/2\pi/2 sind identisch; der einzige Unterschied besteht darin, dass wir direkt einen Winkel wählen, statt eine Pulslänge zu kalibrieren. Auf Hardware mit fraktionalen Gates läuft RX(θ)R_X(\theta) für beliebiges θ\theta nativ.

Eine Ansicht auf der Bloch-Kugel​

Die Animation unten zeigt die Nutation: Ein resonanter Antrieb dreht den Bloch-Vektor gleichmäßig um die xx-Achse und führt ihn von ∣0⟩|0\rangle in Richtung ∣1⟩|1\rangle und zurück. Genau diese Bewegung misst die Rabi-Kurve.

Das Problem auf einen Circuit abbilden​

Wir bauen einen parametrisierten Circuit, RX(θ)R_X(\theta) gefolgt von einer Messung, und werten ihn später bei vielen Winkeln gleichzeitig aus.

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")

Output of the previous code cell

Für die Ausführung optimieren​

Transpiliere zu einem Circuit der Instruction Set Architecture (ISA) für das gewählte Backend.

rabi_isa = pm_q.run(rabi)

Ausführen​

Wir variieren θ\theta von 00 bis 4π4\pi (zwei volle Rabi-Zyklen). Wenn wir ein Array von Parameterwerten als einzelnen Primitive Unified Bloc (PUB) übergeben, werden alle in einem Job ausgeführt.

angles = np.linspace(0, 4 * np.pi, 41)
job = sampler.run([(rabi_isa, angles.reshape(-1, 1))], shots=1000)
rabi_result = job.result()

Nachbearbeitung und Analyse​

Extrahiere P1P_1 bei jedem Winkel und vergleiche mit sin⁡2(θ/2)\sin^2(\theta/2).

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()

Output of the previous code cell

Teste dein Verständnis​

Auf der gemessenen Kurve erreicht P1P_1 sein Maximum zum ersten Mal beim π\pi-Puls (θ=π\theta = \pi). Nimm nun an, ein Gerät ist falsch kalibriert, sodass ein nomineller Winkel θ\theta auf der x-Achse tatsächlich eine Rotation von 1.05 θ1.05\,\theta erzeugt.

(a) Bei welchem nominellen Winkel würde das erste Maximum von P1P_1 jetzt auftreten? (b) Wäre ein einzelner π\pi-Puls eine empfindliche Methode, um diesen 5%-Fehler zu erkennen?

Antwort

(a) Das Maximum erscheint dort, wo die tatsächliche Rotation π\pi entspricht, also beim nominellen Winkel θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — eine kleine Verschiebung des Peaks nach links.

(b) Nein. Ein nomineller π\pi-Puls erzeugt eine tatsächliche Rotation von 1.05π1.05\pi und ergibt P1=sin⁡2(1.05π/2)≈0.994P_1 = \sin^2(1.05\pi/2) \approx 0.994 statt 11 — eine Änderung, die zu klein ist, um sie vor dem Messrauschen zu erkennen. Deshalb verstärken NMR-Spektroskopiker den Fehler, um präzise zu kalibrieren, indem sie die Rotation viele Male anwenden (zum Beispiel mit der 360∘360^\circ-Methode oder einer langen Pulsfolge), sodass sich ein kleiner Fehler pro Puls zu einer großen, messbaren Abweichung aufsummiert. Auf diese Idee kommen wir in den Challenge-Aufgaben zurück.

Experiment: T1T_1 (Spin-Gitter-Relaxation)​

Theorie​

Sich selbst überlassen gibt ein angeregter Spin Energie an seine Umgebung (das „Gitter") ab und relaxiert zurück in Richtung des thermischen Gleichgewichts. Die Besetzung des angeregten Zustands zerfällt exponentiell mit der Spin-Gitter-Relaxationszeit T1T_1:

P1(τ)=e−τ/T1.P_1(\tau) = e^{-\tau/T_1}.

Für Chemiker legt T1T_1 die Recycle-Verzögerung fest, die du zwischen den Scans für quantitative NMR abwarten musst, und sie gibt Aufschluss über molekulares Taumeln und Dynamik. Bei einem Qubit ist sie die Energierelaxationszeit und eine der beiden wichtigsten Größen, mit denen die Dekohärenzeffekte in einem Quantencomputer modelliert werden.

Das Experiment. Präpariere ∣1⟩|1\rangle mit einem XX-Gate (ein π\pi-Puls), warte eine variable Verzögerung τ\tau und miss dann. Der Anteil, der noch in ∣1⟩|1\rangle ist, zeichnet den Zerfall nach.

Eine Bloch-Kugel-Ansicht​

Die T1T_1-Relaxation bringt den angeregten Zustand zurück ins Gleichgewicht, sodass der Bloch-Vektor entlang der zz-Achse von ∣1⟩|1\rangle zurück nach ∣0⟩|0\rangle gleitet.

Eine Verzögerungsreihe erstellen und transpilieren​

Verzögerungsdauern müssen konkrete Werte sein (sie werden in Hardwarezeit geplant), daher erstellen wir pro Verzögerung ein Circuit.

# 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

Ausführen​

t1_job = sampler.run([(c,) for c in t1_isa], shots=1000)
t1_res = t1_job.result()

Analyse — die Exponentialfunktion anpassen​

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")

Output of the previous code cell

Measured T1 = 225.3 microseconds

Die genaue T1T_1-Zeit variiert von Experiment zu Experiment und von Backend zu Backend, aber du solltest ein Ergebnis von etwa einigen hundert Mikrosekunden sehen.

Teste dein Verständnis​

Bei quantitativer NMR soll man zwischen den Scans etwa 5 T15\,T_1 warten. Welcher Bruchteil der Gleichgewichtsmagnetisierung hat sich nach 5 T15\,T_1 gemäß dem Zerfallsgesetz erholt?

Antwort

Nach 5 T15\,T_1 beträgt die verbleibende Abweichung vom Gleichgewicht e−5≈0.0067e^{-5}\approx 0.0067, das heißt, etwa 99.3% sind erholt.

Experiment: T2T_2, T2∗T_2^*​

Theorie​

T1T_1 misst den Energieverlust. T2T_2 misst den Verlust der Phasenkohärenz in der Transversalebene. Das können wir mit einem Ramsey-Experiment beobachten: Ein π/2\pi/2-Puls dreht alle Spins zum Äquator hinunter, alle zeigen in dieselbe Richtung. Sie führen eine Larmor-Präzession aus, aber jeder präzediert mit einer etwas anderen Frequenz, sodass sie sich entlang des Äquators auffächern und das transversale Nettosignal mit der charakteristischen Zerfallszeit T2∗T_2^* abnimmt.

Hinweis für Quanten-Leser: Das Auffächerungsbild zeigt eine Möglichkeit, Kohärenz zu verlieren: eine Streuung definierter Phasen, die sich herausmitteln. Es ist wichtig zu wissen, dass dies nicht der einzige Mechanismus ist. Kohärenz kann auch verloren gehen, weil die Phase mit der Umgebung verschränkt wird und damit überhaupt kein definierter Wert mehr ist, den das System besitzt. Man kann die beiden Mechanismen nicht allein anhand des Signalzerfalls unterscheiden, daher ist das Auffächerungsbild eine gute Intuition. Achte darauf, es nicht als vollständige oder wörtliche Geschichte zu verstehen.

Zwei Effekte bewirken die Dephasierung eines Spins:

  • Irreversible (oder effektiv irreversible) Dekohärenz durch unkontrollierte Kopplung an andere Freiheitsgrade, charakterisiert durch das echte T2T_2. In der NMR kommt sie im Allgemeinen von der Molekülbewegung, die die Kopplungen zwischen Spins moduliert. Im Quantencomputing entspricht sie unerwünschten Kopplungen zwischen Qubits und der Umgebung.
  • Statische oder langsam veränderliche, reversible Frequenzverschiebungen, etwa durch Feldinhomogenität, Frequenzunterschiede von Qubit zu Qubit und Kalibrierungsfehler. Zusammen mit dem echten T2T_2 ergeben sie die kürzere scheinbare Zerfallszeit T2∗T_2^*, die in einem Ramsey-Experiment beobachtet wird.

Wie gewinnen wir also das echte T2T_2 zurück, wenn ein Ramsey-Experiment nur T2∗T_2^* liefert? Das zweite Experiment, das sogenannte Hahn-Echo, hebt das gesamte Rauschen der zweiten Art auf, um das echte T2T_2 zu finden. Es beginnt wie ein Ramsey-Experiment mit einem π/2\pi/2-Puls, dann fächern sich die Spins wie zuvor auf. Nach einer Zeit τ\tau wird ein π\pi-Puls angewendet. Der Puls kehrt die durch statische Frequenzverschiebungen verursachte Phasenakkumulation um, sodass sich die Spins nach einer zweiten Verzögerung gleicher Dauer τ\tau wieder refokussieren.

T2∗T_2^* und T2T_2 sind beide wichtige Größen, sowohl in der Chemie als auch im Quantencomputing. In der Chemie bestimmt T2∗T_2^* die Linienbreite bei einem Standard-NMR-Experiment (was das bedeutet, siehst du in einem späteren Abschnitt). T2T_2 kann viel über die Moleküle verraten, etwa über molekulares Taumeln, und eine Änderung von T2T_2 kann anzeigen, dass sich ein Molekül an etwas Größeres gebunden hat. Im Quantencomputing wollen wir im Allgemeinen, dass sowohl T2T_2 als auch T2∗T_2^* möglichst lang sind, damit wir mehr Zeit für Experimente haben, bevor die Dekohärenz das System überwältigt. Eine ganze Familie von Techniken, die auf dem Hahn-Echo beruhen, verlängert diese Zeit, indem sie das reversible Rauschen aufhebt, sodass das Circuit idealerweise durch das echte T2T_2 begrenzt ist, nicht durch T2∗T_2^*. Das nennt man „dynamische Entkopplung".

Implementierung​

Wenn wir die Ramsey- und Hahn-Echo-Experimente auf einem Qubit implementieren, unterscheidet sich die Auslesung etwas vom NMR-Fall. In der NMR kannst du die oszillierenden Felder direkt beobachten, die durch die in der Transversalebene mit der Larmor-Frequenz präzedierenden Spins induziert werden. Bei einem Qubit können wir aber nur die Projektion entlang der ZZ-Achse messen, das heißt, ob es ∣0⟩\vert 0 \rangle oder ∣1⟩\vert 1 \rangle ist. Daher beenden wir beide Experimente mit einem zusätzlichen π/2\pi/2-Puls, gefolgt von einer Messung. Dieser π/2\pi/2-Puls übersetzt die Phaseninformation des Qubits in eine Besetzung.

Im Folgenden führen wir jedes Experiment aus, um T2∗T_2^* und T2T_2 zu bestimmen.

Ramsey (T2∗T_2^{*}): Zwei π/2\pi/2-Pulse mit Verzögerungszeit τ\tau​

Der erste π/2\pi/2-Puls kippt den Bloch-Vektor zum Äquator, und der nächste übersetzt die Phase des Bloch-Vektors in eine Besetzung. Weil sich die Phase mit der Zeit ändert, während der Bloch-Vektor präzediert, sehen wir ein sinusförmiges Muster in der Besetzung als Funktion der Präzessionszeit τ\tau. Wenn das Qubit die Phasenkohärenz verliert, sollten wir sehen, dass die Amplitude dieser Sinuskurve abnimmt.

Einige weitere Feinheiten für Quanten-Leser: In Quantencomputing-Experimenten liegen die beobachteten Oszillationen nicht tatsächlich bei der Larmor-Frequenz, die im GHz-Bereich liegt und viel zu schnell ist, um aufgelöst zu werden. Quantenexperimente arbeiten im rotierenden Bezugssystem: einem System, das sich mit dem Qubit bei (nahe) der Larmor-Frequenz mitdreht, sodass die schnelle Präzession unsichtbar ist und der Bloch-Vektor nahezu stillsteht. Um Interferenzstreifen zu sehen, verstimmen wir unser rotierendes Bezugssystem absichtlich leicht gegenüber der Larmor-Frequenz. Der Bloch-Vektor scheint dann mit dieser kleinen Verstimmung zu präzedieren, und wir erhalten ein oszillierendes Signal bei der Verstimmungsfrequenz. Im Folgenden legen wir die Verstimmung nicht auf einen festen Wert fest, sondern wählen sie so, dass etwa ein Dutzend Streifen in das Messfenster passt (das wir an die Kohärenzzeit des Qubits anpassen), wodurch die Oszillation ohne Aliasing leicht abgetastet werden kann. Noch eine Besonderheit: In Qiskit haben wir nicht die Steuerung auf Pulsebene, die nötig wäre, um die Pulsfrequenz zu ändern (die die Frequenz des rotierenden Bezugssystems festlegt). Stattdessen fügen wir von Hand eine effektive Verstimmung hinzu und nutzen sie, um die Phase des zweiten Ramsey-Pulses zu setzen. Das ist etwas konstruiert, aber das Ergebnis ist dasselbe, und der beobachtete Zerfall ist weiterhin das echte T2∗T_2^*.

Eine Bloch-Kugel-Ansicht​

Ein π/2\pi/2-Puls kippt die Spins zum Äquator; jeder präzediert dann mit einer etwas anderen Rate und sie fächern sich auf, sodass der (rote) Nettovektor schrumpft, während sie dephasieren — dieser Zerfall ist T2∗T_2^*.

# 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)

Output of the previous code cell

Hahn-Echo (T2T_2): Einen refokussierenden π\pi-Puls in der Mitte hinzufügen​

Eine Bloch-Kugel-Ansicht​

Das Hahn-Echo: Die Spins fächern sich wie zuvor auf, dann kippt ein π\pi-Puls sie um, sodass ihre fortgesetzte Präzession sie wieder refokussiert (der rote Vektor wächst wieder), und die durch statische Dephasierung verlorene Kohärenz wird zurückgewonnen.

# 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

Output of the previous code cell

Analyse: Die beiden Zerfälle anpassen und vergleichen​

# 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")

Output of the previous code cell

T2* (Ramsey) = 171.2 µs ; T2 (echo) = 326.0 µs

Hier sehen wir wie erwartet sowohl die Ramsey-Oszillation als auch den Zerfall des Hahn-Echo-Signals. Die zugehörigen Zeitskalen dieser Zerfälle sind T2∗T_2^* beziehungsweise T2T_2.

Teste dein Verständnis​

Typischerweise solltest du T2≳T2∗T_2 \gtrsim T_2^{*} finden. Erkläre in einem Satz, warum, und sage, welchen physikalischen Effekt der π\pi-Puls des Hahn-Echos entfernt und welchen er nicht entfernen kann.

Antwort

T2∗T_2^{*} enthält sowohl irreversible Dephasierung als auch reversible statische Streuung und zerfällt daher schneller; der π\pi-Puls des Echos refokussiert die statische, reversible Dephasierung (Feldinhomogenität / feste Frequenzverschiebungen) und lässt nur die irreversible Spin-Spin-Dephasierung übrig, die das echte T2T_2 ist. Das Echo kann Fluktuationen nicht rückgängig machen, die sich zwischen den beiden Hälften der Sequenz ändern.

NMR-Spektroskopie eines Moleküls: Chemische Verschiebung und JJ-Kopplung​

Von einem Spin zu einem Molekül​

Die bisherigen Experimente charakterisierten einen einzelnen, isolierten Spin. Echte chemische NMR liest ein Molekül aus (viele gekoppelte Spins gleichzeitig) und nutzt das resultierende Spektrum, um Merkmale der Molekülstruktur abzuleiten. Das kann uns über zwei Effekte Aufschluss geben:

  • Chemische Verschiebung. Jeder chemisch unterscheidbare Kern präzediert mit einer etwas anderen Frequenz νi\nu_i, weil seine lokalen Elektronen das angelegte Feld unterschiedlich stark abschirmen. Die Positionen der Peaks identifizieren daher chemische Umgebungen (zum Beispiel ein CH3_3-Proton im Gegensatz zu einem aromatischen).

  • Skalare JJ-Kopplung. Eine Wechselwirkung über Bindungen zwischen zwei Spins spaltet jeden Peak in ein Multiplett auf. Ein Kern mit nn äquivalenten Nachbarn wird in n+1n+1 Linien mit binomialen Intensitäten aufgespalten, die n+1n+1-Regel. Das Muster der Aufspaltung zeigt die Konnektivität (welche Kerne gebunden sind), und der Abstand ist die Kopplungskonstante JJ.

Das Experiment​

Um dies auf unserem Quantencomputer zu modellieren, müssen wir einen Spin-Hamiltonian auf den Qubits implementieren. Im Grenzfall schwacher Kopplung und hohen Felds (Unterschiede der chemischen Verschiebung ≫\gg Kopplungen) mitteln sich die XXXX- und YYYY-Anteile der Kopplung heraus, und es bleibt nur ZZZZ. Der Hamiltonian lautet also (in Einheiten der Winkelfrequenz, ℏ=1\hbar=1):

H=∑iπ νi Zi⏟chemical shift  +  ∑i<jπ2 Jij ZiZj⏟J-coupling.H = \underbrace{\sum_i \pi\,\nu_i\, Z_i}_{\text{chemical shift}} \;+\; \underbrace{\sum_{i<j} \tfrac{\pi}{2}\,J_{ij}\, Z_iZ_j}_{J\text{-coupling}}.

Jeder Kern ist ein Qubit: Die chemische Verschiebung ist eine ZZ-Rotation auf einem Qubit, und die Kopplung ist eine ZZZZ-Wechselwirkung zwischen zwei Qubits.

Das Experiment läuft wie folgt ab. Ein π/2\pi/2-Puls kippt jeden Spin in die Transversalebene, und die Spins präzedieren und koppeln für die Zeit tt. Wir zeichnen die komplexe transversale Magnetisierung s(t)=⟨X⟩+i⟨Y⟩s(t) = \langle X\rangle + i\langle Y\rangle auf, summiert über alle Spins. Wie im Ramsey-Experiment oben zeichnet diese transversale Magnetisierung als Funktion der Präzessionszeit tt eine abklingende Sinuskurve nach. Wenn wir die Fourier-Transformation des Signals bilden, erhalten wir ein Spektrum mit Peaks bei der chemischen Verschiebung νi\nu_i, aufgespalten durch die skalare Kopplung JijJ_{ij}.

hinweis

Wir simulieren die Entwicklung des Moleküls, indem wir die Zeit tt auf Rotationswinkel abbilden: Ein größeres tt bedeutet größere ZZ- und ZZZZ-Rotationen, nicht ein längeres oder tieferes Circuit. Der simulierte Zustand schreitet zwar mit tt voran (das lässt den freien Induktionszerfall (FID) oszillieren), aber die physikalischen Qubits sind nie länger untätig, und es gibt keine von tt abhängige Dekohärenz. Der simulierte FID zerfällt daher von selbst nie. Bei einem unendlich langen idealen FID wären die Spektrallinien unendlich scharf; unser endliches Abtastfenster gibt ihnen stattdessen eine endliche Auflösung und kann spektrales Leakage verursachen. Echte NMR-Linien haben eine endliche Breite, die durch T2∗T_2^* bestimmt wird. Wir fügen sie wieder hinzu, indem wir den FID vor der Fourier-Transformation mit einem Fenster e−t/T2∗e^{-t/T_2^{*}} multiplizieren. Das nennt man Linienverbreiterungs-Apodisierung; Spektroskopiker wenden sie bei der Verarbeitung an, um die Linienbreite festzulegen (Auflösung wird gegen Signal-Rausch-Verhältnis getauscht). (Eine optionale Zelle im Anhang dieses Moduls zeigt eine Trotterisierte Alternative, bei der der Zerfall stattdessen von echter Dekohärenz stammt, die mit der Circuit-Tiefe wächst.)

Das AX-System​

Beginnen wir mit dem sogenannten AX-System, einem Molekül aus zwei gekoppelten, chemisch unterscheidbaren Spins. Die Bezeichnungen A und X stehen für die beiden chemisch unterschiedlichen Spin-Umgebungen. Die Buchstaben sind willkürlich, aber dass sie im Alphabet weit auseinanderliegen, veranschaulicht ihre chemische Verschiedenheit.

Abbilden und optimieren​

Wir schreiben eine Funktion, die für gegebene Offsets der chemischen Verschiebung νi\nu_i und Kopplungen JijJ_{ij} ein parametrisiertes Circuit zur Zustandspräparation zurückgibt. Der π/2\pi/2-Puls kippt die Spins zum Äquator, dann simulieren die ZZ-Rotationen die Entwicklung des Zustands unter den Termen der chemischen Verschiebung und der JJ-Kopplung. Jedes νi\nu_i ist ein kleiner Präzessions-Offset in Hz relativ zu einem gewählten Bezugspunkt oder Träger des rotierenden Bezugssystems. Hier legen wir ihn manuell fest; in einem echten Molekül verändert die Elektronenabschirmung das lokale Magnetfeld und verschiebt die Resonanz jedes Kerns relativ zu diesem Bezugspunkt. Dies wird durch die RZR_Z-Gates auf einem Qubit modelliert, eines pro Spin, und jedes νi\nu_i legt fest, wo der Peak dieses Spins im Spektrum liegt. Das ZZZZ-Gate auf zwei Qubits wendet die Kopplung JijJ_{ij} an, die jeden Peak in ein Multiplett aufspaltet und den Abstand zwischen seinen Linien bestimmt.

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")

Ausführen und nachbearbeiten​

Wir werten das Circuit über ein Raster von Entwicklungszeiten aus, und der Estimator liefert jeweils ⟨X⟩\langle X\rangle und ⟨Y⟩\langle Y\rangle. Wir kombinieren diese zum komplexen FID ⟨X⟩+i⟨Y⟩\langle X\rangle + i\langle Y\rangle, wenden das Apodisierungsfenster an und Fourier-transformieren das Ergebnis, um das Spektrum zu erhalten.

# --- 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)}),
)

Output of the previous code cell

Expected AX lines (Hz): [-7.0, -5.0, 5.0, 7.0]

Erweiterung: Ein AX2_2-System und die n+1n+1-Regel​

Füge nun einen zweiten äquivalenten Nachbarn hinzu. In einem AX2_2-System koppelt ein A-Spin mit demselben JJ an zwei äquivalente X-Spins. (Denk daran: X ist nur eine Bezeichnung für die beiden äquivalenten Spins; es hat nichts mit den Pauli-XX-Operatoren zu tun. Jede Kopplung unten ist weiterhin ZZZZ.) Spin A sieht dann vier Nachbarkonfigurationen (↑↑, ↑↓, ↓↑, ↓↓)(\uparrow\uparrow,\ \uparrow\downarrow,\ \downarrow\uparrow,\ \downarrow\downarrow); die beiden gemischten fallen zusammen, sodass A in ein 1:2:1-Triplett aufgespalten wird (n=2⇒n+1=3n=2 \Rightarrow n+1 = 3 Linien). Jeder X-Spin sieht nur A und bleibt ein Dublett. Äquivalente Spins spalten sich nicht gegenseitig auf, daher nehmen wir keine X–X-Kopplung auf.

Ein echtes Molekül mit diesem Muster ist 1,1,2-Trichlorethan (Cl2_2CH–CH2_2Cl). Bei Betrachtung der Wasserstoff-Spins (die häufigste Form der NMR) spielt das einzelne Wasserstoff-Proton in CH die Rolle von A, und die beiden äquivalenten CH2_2-Protonen sind die beiden X-Spins. Zähle nun einfach die Nachbarn: Das CH-Proton hat zwei Nachbarn (das CH2_2-Paar), daher spaltet sein Signal in ein 1:2:1-Triplett auf (n=2n=2); jedes CH2_2-Proton hat nur einen Nachbarn (das CH), daher ist das CH2_2-Signal ein 1:1-Dublett (n=1n=1). Beachte, dass die Gruppe mit weniger Protonen (CH) mehr Linien zeigt; die Linienzahl spiegelt wider, wie viele Nachbarn ein Signal hat, nicht wie viele Protonen es teilen.

1,1,2-trichloroethane: the CH proton is A, the two equivalent CH_2 protons are X

Im Folgenden simulieren wir ein AX2_2-System mit drei 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)],
)

Teste dein Verständnis​

Lies jede Frage, überlege dir deine Antwort und klicke dann auf das Dreieck, um die Lösung anzuzeigen.

Im AX-Spektrum sind die beiden Dubletts (Mitte zu Mitte) durch 12 Hz getrennt, und die beiden Linien innerhalb jedes Dubletts liegen 2 Hz auseinander. Welche Zahl ist der Unterschied der chemischen Verschiebung und welche die Kopplungskonstante JJ? Wenn du zu einem Spektrometer mit doppeltem Magnetfeld wechselst, welche der beiden würde sich ändern?

Antwort

Der Abstand von 12 Hz (Mitte zu Mitte) ist der Unterschied der chemischen Verschiebung (ν1−ν2\nu_1-\nu_2); der Linienabstand von 2 Hz ist die Kopplungskonstante JJ. Chemische Verschiebungen entstehen durch die Abschirmung von B0B_0, sie skalieren also mit dem Feld. Bei doppeltem Feld verdoppelt sich der Abstand auf 24 Hz. Die Kopplung JJ ist eine Moleküleigenschaft über Bindungen und feldunabhängig, sie bleibt also bei 2 Hz. Chemiker unterscheiden Verschiebung von Kopplung, indem sie beobachten, wie eine Aufspaltung mit dem Feld skaliert.

Sage mithilfe der n+1n+1-Regel das A-Multiplett voraus, wenn ein dritter äquivalenter X-Spin hinzugefügt würde (ein AX3_3-System), und erkläre, warum die beiden äquivalenten X-Spins in AX2_2 sich nicht gegenseitig aufspalten.

Antwort

Bei n=3n=3 äquivalenten Nachbarn wird A zu einem 1:3:3:1-Quartett. Äquivalente Spins spalten sich nicht gegenseitig auf, weil ihre Kopplung die betreffenden Energieniveaus alle um denselben Betrag verschiebt, sodass sich die Abstände zwischen diesen Niveaus — die Frequenzen, die wir sehen — nicht ändern.

Fazit​

Wir haben einen echten Quantencomputer verwendet, um zu untersuchen, wie Kernspins in einem NMR-Experiment präzedieren und dekohärieren und wie man die resultierenden Spektren interpretiert. Es lohnt sich, einen Schritt zurückzutreten und zu überprüfen, warum das überhaupt möglich war und warum es mehr als eine Kuriosität ist.

Ein Qubit und ein Spin-1/2-Kern sind mathematisch dasselbe Objekt: Zwei-Niveau-Quantensysteme auf der Bloch-Kugel, gedreht durch resonante Pulse, präzedierend um eine Achse und Kohärenz an ihre Umgebung verlierend. Das gesamte Vokabular dieses Moduls (die Larmor-Frequenz, das rotierende Bezugssystem, T1T_1, T2T_2 und T2∗T_2^*, Ramsey-Streifen, das Hahn-Echo) gehört gleichermaßen zur NMR und zum Quantencomputing. Was ein Chemiker chemische Verschiebung nennt, nennt ein Quanteningenieur eine ZZ-Rotation auf einem Qubit; was ein Chemiker JJ-Kopplung nennt, nennt ein Quanteningenieur eine ZZZZ-Wechselwirkung. Die beiden Felder haben dieselbe Physik mit unterschiedlichen Worten beschrieben.

Diese Verbindung ist historisch, nicht nur konzeptionell: Die NMR war eine der ersten Plattformen, auf denen echte Quantenalgorithmen liefen. Bis 2001 hatte ein Molekül in Lösung den Shor-Algorithmus ausgeführt, um 15 zu faktorisieren, wobei jeder Kernspin als Qubit diente. Die NMR hat dem Quantencomputing außerdem einen Großteil seines Steuerungswerkzeugkastens geliefert. Das Spin-Echo, CPMG-Pulsfolgen, zusammengesetzte Pulse und dynamische Entkopplung wurden alle von Spektroskopikern erfunden, Jahrzehnte bevor Qubits sie zur Bekämpfung der Dekohärenz brauchten. Es liegt eine befriedigende Symmetrie in dem, was wir gerade getan haben: Das Feld, das frühe Quantencomputer hervorgebracht hat, ist nun etwas, das ein Quantencomputer simulieren kann.

Rückblick und Fragen​

Zentrale Konzepte​

  • Ein Qubit ist ein technisch hergestelltes Zwei-Niveau-System, äquivalent zu einem Spin-1/2; sein ∣0⟩/∣1⟩|0\rangle/|1\rangle, seine Frequenz, Gates, T1T_1 und T2T_2 sind die direkten Entsprechungen von NMR-Größen.

  • Ein resonanter Puls dreht den Bloch-Vektor; der Flipwinkel bestimmt die Rotation, und die Rabi-/Nutationskurve P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) identifiziert π\pi- und π/2\pi/2-Rotationen.

  • T1T_1 ist die Energie-Relaxation (Spin-Gitter); T2T_2 ist die Phasen-Relaxation (Spin-Spin); T2∗T_2^{*} umfasst zusätzlich reversible statische Dephasierung, die ein Hahn-Echo refokussiert.

  • Ein Molekül ist eine Menge gekoppelter Spins: Die chemische Verschiebung legt die Position jedes Peaks fest, und die JJ-Kopplung spaltet Peaks nach der n+1n+1-Regel in Multipletts auf, die zusammen chemische Umgebung und Konnektivität kodieren.

Wahr / falsch​

  1. W/F Die Larmor-Frequenz ist der Energieabstand zwischen den beiden Spinzuständen geteilt durch ℏ\hbar.

  2. W/F Ein 90∘90^\circ-Puls (π/2\pi/2) invertiert einen Spin im Gleichgewicht vollständig.

  3. W/F T2∗T_2^{*} ist im Allgemeinen länger als T2T_2.

  4. W/F Das Hahn-Echo refokussiert irreversible Spin-Spin-Dephasierung.

  5. W/F In einem AX2_2-System erscheint der A-Kern als 1:2:1-Triplett.

  6. W/F Die Kopplungskonstante JJ verdoppelt sich, wenn du das Magnetfeld des Spektrometers verdoppelst.

  7. W/F Das Entfernen von Qiskit Pulse macht es unmöglich, T1T_1 auf aktueller Hardware zu messen.

Kurze Antwort​

  1. Erkläre in zwei Sätzen, warum ein einzelner π\pi-Puls eine schlechte Methode ist, um einen Flipwinkelfehler von 5%5\% zu erkennen, und wie das Aneinanderreihen von Pulsen dies behebt.

  2. Eine Probe zeigt breite NMR-Linien. Was sagt dir das in Bezug auf T2T_2, und welche Qubit-Größe ist das Analogon?

  3. Nenne das Qubit-Analogon von jedem: (a) die Recycle-Verzögerung, (b) der 90∘90^\circ-Puls, (c) die chemische Verschiebung.

  4. Erkläre mithilfe der n+1n+1-Regel, warum eine Ethylgruppe (CH3_3CH2_2–) ein 1:2:1-Triplett und ein 1:3:3:1-Quartett zeigt, und gib an, welche Gruppe welches Multiplett erzeugt.

Challenge-Aufgaben​

  1. Carr-Purcell-Folge. Ändere das Hahn-Echo so, dass nn gleichmäßig verteilte π\pi-Pulse eingefügt werden (eine Folge im CPMG-Stil), und miss T2T_2 als Funktion von nn. Erörtere, warum mehr refokussierende Pulse das scheinbare T2T_2 verlängern können.

  2. Verstärkung von Pulsfehlern. Erstelle ein Circuit, das RX(π)R_X(\pi)  n\,n-mal auf ∣0⟩|0\rangle anwendet, und trage P1P_1 gegen nn für einen absichtlich falsch eingestellten Winkel RX(1.05π)R_X(1.05\pi) auf. Zeige, wie der Fehler mit wachsendem nn sichtbar wird. Tipp: Setze nach jeder Rotation eine Barrier; andernfalls kann der Transpiler die wiederholten Rotationen zu einem einzigen äquivalenten Gate zusammenfassen.

  3. Ethylgruppe. Erstelle mit fid_circuit das Protonenspektrum eines Ethylfragments (CH3_3–CH2_2): fünf Qubits, wobei die drei CH3_3-Protonen eine chemische Verschiebung teilen und die zwei CH2_2-Protonen eine andere, und jedes CH3_3–CH2_2-Paar mit demselben JJ gekoppelt ist (keine Kopplung innerhalb einer äquivalenten Gruppe). Sage das Muster mit der n+1n+1-Regel voraus und simuliere es dann; du solltest ein 1:3:3:1-Quartett (CH2_2) und ein 1:2:1-Triplett (CH3_3) erhalten.

Anhang (optional): Trotterisierte Entwicklung — die Dekohärenz die Linienbreite bestimmen lassen​

Im Abschnitt NMR-Spektroskopie eines Moleküls wird die Entwicklung e−iHte^{-iHt} in einer einzigen Schicht angewendet, deren Tiefe nicht mit tt wächst, sodass der FID nicht von selbst zerfällt und wir die Linienbreite mit dem Apodisierungsfenster e−t/T2e^{-t/T_2} vorgeben. Diese optionale Zelle wählt den umgekehrten Ansatz. Sie zerlegt die Entwicklung in pp Trotter-Schritte und setzt nach jedem Schritt eine Barrier (damit der Transpiler sie nicht wieder zu einer Schicht zusammenfassen kann). Zeitpunkt kk verwendet kk Schritte, sodass die Circuit-Tiefe mit der Zeit wächst.

Da alle Terme in HH kommutieren, ist dies exakt: Auf einem rauschfreien Simulator ergibt es dieselben scharfen Peaks. Aber auf einem Backend mit Rauschmodell oder echter Hardware lässt der sich akkumulierende Gate-Fehler den FID von selbst zerfallen, und dieser Zerfall bestimmt die Linienbreite. Dies ist die Idee T2↔T_2\leftrightarrowLinienbreite aus dem Abschnitt T2T_2, T2∗T_2^*-Experiment, nun aber aus echter Dekohärenz statt aus einem Verarbeitungsfenster. Wir führen auf diese Weise sowohl das AX-System (zwei Dubletts) als auch das AX2_2-System (ein 1:2:1-Triplett plus ein Dublett) aus.

Zwei praktische Hinweise. Erstens sind die Kosten die Tiefe: Das tiefste AX-Circuit verwendet ∼ ⁣47\sim\!47 Gates auf zwei Qubits und die AX2_2-Version etwa doppelt so viele, sodass beide weitaus verrauschter und teurer sind als die exakte Version. Zweitens wird jedes Spinsystem weiterhin als ein Job übermittelt, und jeder Zeitpunkt verwendet dasselbe Layout physischer Qubits, das im Spektroskopie-Abschnitt gewählt wurde. Wenn USE_BATCH=True, nutzen diese Jobs auch den optionalen notebookweiten Batch.

# 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()