Przejdź do głównej treści
Ten projekt jest w wersji beta. Znalazłeś błąd? Otwórz zgłoszenie na GitHub. Masz pytanie lub pomysł? Zapytaj w Discussions — chętnie poznamy Twoją opinię!
Jak oceniasz jakość tłumaczenia tej strony?

Jądrowy rezonans magnetyczny z Qiskit

W tym module Qiskit in Classrooms uczniowie muszą mieć działające środowisko Python z zainstalowanymi następującymi pakietami:

  • qiskit v2.1 lub nowszy
  • qiskit-ibm-runtime v0.40.1 lub nowszy
  • qiskit-aer v0.17 lub nowszy
  • scipy i matplotlib (do dopasowywania i rysowania wykresów)
  • pylatexenc (do renderowania diagramów obwodów)

Aby skonfigurować środowisko i zainstalować powyższe pakiety, zobacz przewodnik Install Qiskit. Aby uruchamiać zadania na prawdziwych komputerach kwantowych, uczniowie muszą założyć konto IBM Quantum®, wykonując kroki z przewodnika Set up your IBM Cloud® account.

Ten moduł został przetestowany i zużywa około 5 minut czasu QPU. To jest szacunek, a Twoje rzeczywiste zużycie może się różnić.

Wprowadzenie​

Gdy chemik przeprowadza eksperyment NMR, umieszcza próbkę spinów jądrowych w silnym polu magnetycznym, przykłada impuls o częstotliwości radiowej (RF) i mierzy powracający sygnał. Technika opiera się na kilku ideach z mechaniki kwantowej cząstki o spinie 1/2 w polu magnetycznym. Spin ma dwa poziomy energetyczne rozdzielone energią odpowiadającą charakterystycznej częstotliwości Larmora. Rezonansowy impuls o tej częstotliwości przechyla spin do płaszczyzny poprzecznej, gdzie ten precesuje i indukuje sygnał, który następnie zanika z dwoma charakterystycznymi czasami, T1T_1 i T2T_2.

Nadprzewodzący kubit to wytworzona inżyniersko wersja tego samego obiektu: kontrolowalnego dwupoziomowego układu kwantowego. Jego stany ∣0⟩|0\rangle i ∣1⟩|1\rangle są odpowiednikami spinu w górę i w dół, bramka, którą stosujemy do jego obrotu, jest odpowiednikiem impulsu RF, a on sam traci koherencję z własnymi T1T_1 i T2T_2. To nie jest tylko luźne porównanie; niektóre z najwcześniejszych eksperymentalnych procesorów kwantowych z końca lat 90. i początku XXI wieku wykorzystywały NMR.

W tym module użyjesz prawdziwego komputera kwantowego do przeprowadzenia tych samych eksperymentów charakteryzacyjnych, które wykonuje spektroskopista NMR (lub inżynier kwantowy kalibrujący sprzęt):

  • Rabi / nutacja — pobudzanie spinu i obserwowanie jego obrotu. W ten sposób kalibruje się obrót o π\pi lub π/2\pi/2.

  • T1T_1 — relaksacja spin-sieć, czyli powrót wzbudzonego spinu do równowagi.

  • T2T_2 — relaksacja spin-spin (defazowanie), mierzona eksperymentem Ramseya i refokusowana echem Hahna.

  • Spektroskopia NMR (przesunięcie chemiczne i sprzężenie JJ) — symulacja małej cząsteczki na kilku kubitach i odczyt jej widma multipletowego w celu odtworzenia łączności za pomocą reguły n+1n+1.

Dla każdej koncepcji najpierw podajemy minimum teorii, a następnie realizujemy eksperyment w Qiskit, zgodnie z czteroetapowym przepływem pracy Qiskit patterns.

Teoria​

Cząstki o spinie 1/2 w polu magnetycznym​

Jądro o spinie 1/2 ma moment magnetyczny μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}, gdzie γ\gamma jest współczynnikiem żyromagnetycznym. W statycznym polu B0B_0 skierowanym wzdłuż osi zz dwa stany spinowe rozszczepiają się energetycznie (efekt Zeemana):

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 .

Przerwa energetyczna definiuje częstotliwość Larmora (rezonansową) ω0=γB0\omega_0 = \gamma B_0. Moment magnetyczny precesuje wokół B0B_0 z tą samą częstotliwością.

Aby obrócić spin, przykładamy oscylujące pole RF o częstotliwości równej ω0\omega_0 lub bliskiej. W układzie wirującym z częstotliwością pobudzania (układ wirujący) rezonansowe pobudzenie obraca wektor Blocha wokół osi leżącej w płaszczyźnie xx-yy. To, jak daleko się obróci (kąt odchylenia), zależy od siły i czasu trwania impulsu. Najważniejsze są dwa przypadki: impuls 90∘90^\circ przechyla spin +1/2+1/2 do płaszczyzny poprzecznej, a impuls 180∘180^\circ obraca go całkowicie do −1/2-1/2.

Tłumaczenie między żargonem NMR a żargonem obliczeń kwantowych​

Kubit jest układem dwupoziomowym, matematycznie identycznym ze spinem 1/2 w polu magnetycznym — więc choć działamy na procesorze kwantowym, a nie na jądrze, fizyka jest ta sama, która leży u podstaw NMR. Różni się tylko słownictwo. Ta tabela pomaga rozmawiać zarówno z chemikami fizykami, jak i ze specjalistami od obliczeń kwantowych.

NMR / spin 1/2Procesor kwantowy (kubit)
Spin w górę / spin w dół∣0⟩\ket{0} / ∣1⟩\ket{1}
Przerwa Zeemana ΔE=ℏω0\Delta E = \hbar\omega_0Energia przejścia kubitu ℏω01\hbar\omega_{01}
Częstotliwość Larmora ω0\omega_0Częstotliwość kubitu ω01\omega_{01}
Rezonansowy impuls RF (kąt odchylenia θ\theta)Jednokubitowa bramka obrotu RX(θ)R_X(\theta)
Impuls 90∘90^\circ / 180∘180^\circRX(π/2)R_X(\pi/2) / RX(π)R_X(\pi)
Relaksacja spin-sieć T1T_1Czas relaksacji energii kubitu T1T_1
Relaksacja spin-spin T2T_2Czas defazowania kubitu T2T_2
NutacjaOscylacje Rabiego
Swobodny zanik indukcji (FID), echo HahnaSekwencje Ramseya, sekwencje echa

Wszystko poniżej to ćwiczenie w mierzeniu prawej kolumny i odczytywaniu jej jako lewej kolumny.

Konfiguracja środowiska Qiskit​

W całym module stosujemy przepływ pracy Qiskit patterns:

  1. Zmapuj problem na obwód kwantowy.

  2. Zoptymalizuj obwód do wykonania (transpilacja do backendu).

  3. Wykonaj za pomocą prymitywu IBM Quantum.

  4. Przetwórz końcowo i przeanalizuj wyniki.

Najpierw ładujemy usługę uruchomieniową i wybieramy najmniej obciążony prawdziwy backend. Jeśli nie zapisano jeszcze danych uwierzytelniających, zobacz stronę dokumentacji o tym, jak zapisać dane logowania.

# 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

Użyj poniższej komórki, aby przełączać się między prawdziwym sprzętem a lokalnym symulatorem w całym notatniku. Uruchamianie na sprzęcie jest sednem tego modułu, ale w razie problemów z łącznością lub użyciem symulator może posłużyć jako zapasowe rozwiązanie. Ustaw USE_HARDWARE = True, aby uruchomić na QPU wybranym powyżej.

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

Eksperyment: oscylacje Rabiego (nutacja)​

Teoria​

Pobudzaj spin w rezonansie, a jego wektor Blocha będzie się równomiernie obracał od bieguna do bieguna — nutuje. Jeśli zastosujemy obrót o kąt odchylenia θ\theta wokół osi xx do kubitu zaczynającego w stanie ∣0⟩|0\rangle, prawdopodobieństwo znalezienia go w stanie ∣1⟩|1\rangle wynosi:

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

To jest krzywa Rabiego. Jej pierwsze maksimum to impuls π\pi (pełna inwersja, impuls 180∘180^\circ); impuls π/2\pi/2 to ćwierć obrotu (impuls 90∘90^\circ, który przechyla spin do płaszczyzny poprzecznej). W NMR ta krzywa służy do kalibracji impulsów.

Zastrzeżenie dotyczące eksperymentu

Dosłowny eksperyment Rabiego przemiata amplitudę lub czas trwania pobudzenia. Wymaga to kontroli na poziomie impulsów, którą usunięto w Qiskit v2.0. Tutaj zamiast tego przemiatamy kąt obrotu θ\theta bramki RX(θ)R_X(\theta). Krzywa i identyfikacja π\pi/π/2\pi/2 są identyczne; jedyna różnica polega na tym, że wybieramy kąt bezpośrednio, zamiast kalibrować długość impulsu. Na sprzęcie z bramkami ułamkowymi bramka RX(θ)R_X(\theta) dla dowolnego θ\theta działa natywnie.

Widok na sferze Blocha​

Poniższa animacja pokazuje nutację: rezonansowe pobudzenie równomiernie obraca wektor Blocha wokół osi xx, przenosząc go z ∣0⟩|0\rangle w stronę ∣1⟩|1\rangle i z powrotem. To właśnie ten ruch mierzy krzywa Rabiego.

Mapowanie problemu na obwód​

Budujemy jeden sparametryzowany obwód, RX(θ)R_X(\theta) z następującym po nim pomiarem, i później obliczamy go dla wielu kątów naraz.

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

Optymalizacja do wykonania​

Dokonaj transpilacji do obwodu architektury zestawu instrukcji (ISA) dla wybranego Backend.

rabi_isa = pm_q.run(rabi)

Wykonanie​

Przeprowadzamy przemiatanie θ\theta od 00 do 4π4\pi (dwa pełne cykle Rabiego). Przekazanie tablicy wartości parametrów jako jednego Primitive Unified Bloc (PUB) uruchamia wszystkie z nich w jednym zadaniu.

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

Przetwarzanie końcowe i analiza​

Wyodrębnij P1P_1 dla każdego kąta i porównaj z 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

Sprawdź, czy rozumiesz​

Na zmierzonej krzywej P1P_1 osiąga po raz pierwszy maksimum przy impulsie π\pi (θ=π\theta = \pi). Załóżmy teraz, że urządzenie jest źle skalibrowane, tak że nominalny kąt θ\theta na osi x w rzeczywistości daje obrót o 1.05 θ1.05\,\theta.

(a) Przy jakim nominalnym kącie pojawi się teraz pierwsze maksimum P1P_1? (b) Czy zastosowanie pojedynczego impulsu π\pi byłoby czułym sposobem wykrycia tego 5% błędu?

Odpowiedź

(a) Maksimum pojawia się tam, gdzie rzeczywisty obrót wynosi π\pi, czyli przy nominalnym kącie θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — niewielkie przesunięcie piku w lewo.

(b) Nie. Nominalny impuls π\pi daje rzeczywisty obrót o 1.05π1.05\pi, co prowadzi do P1=sin⁡2(1.05π/2)≈0.994P_1 = \sin^2(1.05\pi/2) \approx 0.994 zamiast 11 — zmiany zbyt małej, by dostrzec ją na tle szumu pomiarowego. Dlatego aby kalibrować precyzyjnie, spektroskopiści NMR wzmacniają błąd, stosując obrót wiele razy (na przykład metoda 360∘360^\circ lub długi ciąg impulsów), tak że mały błąd pojedynczego impulsu kumuluje się w duże, mierzalne odchylenie. Wrócimy do tego pomysłu w zadaniach wyzwań.

Eksperyment: T1T_1 (relaksacja spin-sieć)​

Teoria​

Pozostawiony sam sobie wzbudzony spin oddaje energię otoczeniu („sieci") i relaksuje z powrotem w kierunku równowagi termicznej. Obsadzenie stanu wzbudzonego zanika wykładniczo z czasem relaksacji spin-sieć T1T_1:

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

Dla chemika T1T_1 wyznacza opóźnienie rekonwersji, jakie trzeba odczekać między skanami w ilościowym NMR, i niesie informację o ruchu obrotowym cząsteczek oraz ich dynamice. W przypadku kubitu jest to czas relaksacji energii i jedna z dwóch głównych wartości używanych do modelowania efektów dekoherencji w komputerze kwantowym.

Eksperyment. Przygotuj ∣1⟩|1\rangle za pomocą bramki XX (impuls π\pi), odczekaj zmienne opóźnienie τ\tau, a następnie zmierz. Ułamek wciąż pozostający w ∣1⟩|1\rangle wyznacza przebieg zaniku.

Widok na sferze Blocha​

Relaksacja T1T_1 przywraca stan wzbudzony do równowagi, więc wektor Blocha zsuwa się z ∣1⟩|1\rangle z powrotem do ∣0⟩|0\rangle wzdłuż osi zz.

Budowa i transpilacja skanu opóźnień​

Czasy opóźnień muszą być konkretnymi wartościami (są planowane w czasie sprzętowym), więc budujemy jeden obwód na każde opóźnienie.

# 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

Wykonanie​

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

Analiza — dopasowanie wykładnicze​

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

Dokładny czas T1T_1 różni się między eksperymentami i między Backend, ale powinieneś uzyskać wynik rzędu kilkuset mikrosekund.

Sprawdź, czy rozumiesz​

W ilościowym NMR zaleca się odczekać około 5 T15\,T_1 między skanami. Korzystając z prawa zaniku, jaka część magnetyzacji równowagowej odbudowuje się po 5 T15\,T_1?

Odpowiedź

Po 5 T15\,T_1 pozostałe odchylenie od równowagi wynosi e−5≈0.0067e^{-5}\approx 0.0067, czyli odbudowało się około 99,3%.

Eksperyment: T2T_2, T2∗T_2^*​

Teoria​

T1T_1 mierzy utratę energii. T2T_2 mierzy utratę spójności fazowej w płaszczyźnie poprzecznej. Możemy to zaobserwować w eksperymencie Ramseya: impuls π/2\pi/2 obraca wszystkie spiny na równik, wszystkie wskazują ten sam kierunek. Wykonują precesję Larmora, ale każdy z nich precesuje z nieco inną częstotliwością, więc rozchodzą się wachlarzowo wzdłuż równika, a sumaryczny sygnał poprzeczny zanika z charakterystycznym czasem zaniku T2∗T_2^*.

Uwaga dla czytelników z kwantów: Obraz rozchodzenia się wachlarzowego pokazuje jeden ze sposobów utraty spójności: rozrzut określonych faz, które się uśredniają. Warto wiedzieć, że nie jest to jedyny mechanizm. Spójność może też zostać utracona, ponieważ faza splątuje się z otoczeniem, więc nie jest już w ogóle określoną wartością, którą system posiada. Nie da się odróżnić tych dwóch mechanizmów, obserwując samo zanikanie sygnału, więc obraz wachlarza to dobra intuicja. Uważaj, by nie traktować go jako pełnej ani dosłownej historii.

Dwa efekty powodują defazację spinu:

  • Nieodwracalna (lub efektywnie nieodwracalna) dekoherencja wynikająca z niekontrolowanego sprzężenia z innymi stopniami swobody, charakteryzowana przez prawdziwe T2T_2. W NMR zwykle pochodzi z ruchu cząsteczek modulującego sprzężenia między spinami. W obliczeniach kwantowych odpowiada niepożądanym sprzężeniom między kubitami a otoczeniem.
  • Statyczne lub wolnozmienne, odwracalne przesunięcia częstotliwości, takie jak niejednorodność pola, zróżnicowanie częstotliwości między kubitami i błędy kalibracji. W połączeniu z prawdziwym T2T_2 dają krótszy pozorny czas zaniku T2∗T_2^* obserwowany w eksperymencie Ramseya.

Jak więc odzyskać prawdziwe T2T_2, skoro eksperyment Ramseya mówi nam tylko o T2∗T_2^*? Drugi eksperyment, zwany echem Hahna, znosi cały szum drugiego rodzaju, aby wyznaczyć prawdziwe T2T_2. Zaczyna się tak samo jak eksperyment Ramseya, od impulsu π/2\pi/2, po czym spiny rozchodzą się wachlarzowo jak poprzednio. Po pewnym czasie τ\tau przykładany jest impuls π\pi. Impuls ten odwraca akumulację fazy spowodowaną statycznymi przesunięciami częstotliwości, więc spiny ogniskują się ponownie po drugim opóźnieniu o tym samym czasie trwania τ\tau.

T2∗T_2^* i T2T_2 to ważne wartości zarówno w chemii, jak i w obliczeniach kwantowych. W chemii T2∗T_2^* wyznacza szerokość linii w standardowym eksperymencie NMR (co to znaczy, zobaczysz w późniejszej sekcji). T2T_2 może wiele powiedzieć o cząsteczkach, na przykład o ich ruchu obrotowym, a zmiana T2T_2 może sygnalizować, że cząsteczka związała się z czymś większym. W obliczeniach kwantowych zwykle chcemy, by zarówno T2T_2, jak i T2∗T_2^* były jak najdłuższe, abyśmy mieli więcej czasu na eksperymenty, zanim dekoherencja przytłoczy system. Cała rodzina technik opartych na echu Hahna wydłuża ten czas, znosząc szum odwracalny, tak że obwód jest w idealnym przypadku ograniczony prawdziwym T2T_2, a nie T2∗T_2^*. Nazywa się to „sprzężeniem dynamicznym" (ang. dynamical decoupling).

Implementacja​

Gdy realizujemy eksperymenty Ramseya i echa Hahna na kubicie, odczyt różni się nieco od przypadku NMR. W NMR można bezpośrednio obserwować oscylujące pola indukowane przez spiny precesujące z częstotliwością Larmora w płaszczyźnie poprzecznej. Natomiast w kubicie możemy zmierzyć tylko jego rzut na oś ZZ, czyli czy jest ∣0⟩\vert 0 \rangle, czy ∣1⟩\vert 1 \rangle. Dlatego w obu eksperymentach kończymy dodatkowym impulsem π/2\pi/2, po którym następuje pomiar. Ten impuls π/2\pi/2 przekłada informację o fazie kubitu na obsadzenie.

Poniżej uruchamiamy każdy eksperyment, aby wyznaczyć T2∗T_2^* i T2T_2.

Ramsey (T2∗T_2^{*}): dwa impulsy π/2\pi/2 z czasem opóźnienia τ\tau​

Pierwszy impuls π/2\pi/2 odchyla wektor Blocha na równik, a następny przekłada fazę wektora Blocha na obsadzenie. Ponieważ faza zmienia się w czasie, gdy wektor Blocha precesuje, widzimy sinusoidalny wzór w obsadzeniu jako funkcję czasu precesji τ\tau. Gdy kubit traci spójność fazową, powinniśmy zobaczyć zanik amplitudy tej sinusoidy.

Kilka dodatkowych subtelności dla czytelników z kwantów: W eksperymentach z obliczeń kwantowych obserwowane oscylacje w rzeczywistości nie mają częstotliwości Larmora, która leży w zakresie GHz i jest zbyt szybka, by ją rozdzielić. Eksperymenty kwantowe działają w układzie wirującym: układzie, który obraca się razem z kubitem z (prawie) częstotliwością Larmora, więc szybka precesja jest niewidoczna, a wektor Blocha wygląda niemal nieruchomo. Aby zobaczyć prążki, celowo ustawiamy nasz układ wirujący z niewielkim odstrojeniem od częstotliwości Larmora. Wektor Blocha zdaje się wtedy precesować z tym małym odstrojeniem i otrzymujemy sygnał oscylujący z częstotliwością odstrojenia. Poniżej nie ustalamy odstrojenia na jednej wartości; wybieramy je tak, aby około tuzin prążków mieściło się w oknie pomiarowym (którego rozmiar dobieramy do czasu spójności kubitu), co ułatwia próbkowanie oscylacji bez aliasingu. Jeszcze jedna komplikacja: w Qiskit nie mamy kontroli na poziomie impulsów potrzebnej do zmiany częstotliwości impulsu (która wyznacza częstotliwość układu wirującego). Zamiast tego dodajemy efektywne odstrojenie ręcznie, wykorzystując je do ustawienia fazy drugiego impulsu Ramseya. Jest to nieco sztuczne, ale wynik jest ten sam, a obserwowany zanik wciąż jest prawdziwym T2∗T_2^*.

Widok na sferze Blocha​

Impuls π/2\pi/2 odchyla spiny na równik; każdy z nich precesuje następnie z nieco inną szybkością i rozchodzą się wachlarzowo, więc sumaryczny (czerwony) wektor kurczy się wraz z ich defazacją — ten zanik to 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

Echo Hahna (T2T_2): dodaj ogniskujący impuls π\pi pośrodku​

Widok na sferze Blocha​

Echo Hahna: spiny rozchodzą się wachlarzowo jak poprzednio, następnie impuls π\pi je odwraca, więc ich dalsza precesja ogniskuje je z powrotem razem (czerwony wektor odrasta), odzyskując spójność utraconą przez defazację statyczną.

# 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

Analiza: dopasowanie i porównanie dwóch zaników​

# 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

Widzimy tu zarówno oscylację Ramseya, jak i zanik sygnału echa Hahna, zgodnie z oczekiwaniami. Związane z nimi skale czasowe tych zaników to odpowiednio T2∗T_2^* i T2T_2.

Sprawdź, czy rozumiesz​

Zwykle powinieneś otrzymać T2≳T2∗T_2 \gtrsim T_2^{*}. Wyjaśnij dlaczego w jednym zdaniu i powiedz, jaki efekt fizyczny usuwa impuls π\pi echa Hahna oraz czego nie potrafi usunąć.

Odpowiedź

T2∗T_2^{*} obejmuje zarówno nieodwracalną defazację, jak i odwracalny rozrzut statyczny, więc zanika szybciej; impuls π\pi echa ogniskuje statyczną, odwracalną defazację (niejednorodność pola / stałe przesunięcia częstotliwości), pozostawiając tylko nieodwracalną defazację spin-spin, czyli prawdziwe T2T_2. Echo nie potrafi cofnąć fluktuacji, które zmieniają się między dwiema połowami sekwencji.

Spektroskopia NMR cząsteczki: przesunięcie chemiczne i sprzężenie JJ​

Od jednego spinu do cząsteczki​

Dotychczasowe eksperymenty charakteryzowały pojedynczy, izolowany spin. Prawdziwe chemiczne NMR odczytuje cząsteczkę (wiele sprzężonych spinów naraz) i na podstawie otrzymanego widma wnioskuje o cechach jej struktury. Może to nam powiedzieć o dwóch efektach:

  • Przesunięcie chemiczne. Każde chemicznie odmienne jądro precesuje z nieco inną częstotliwością νi\nu_i, ponieważ otaczające je elektrony ekranują przyłożone pole w różnym stopniu. Położenia pików identyfikują zatem środowiska chemiczne (na przykład proton CH3_3 w porównaniu z aromatycznym).

  • Skalarne sprzężenie JJ. Oddziaływanie przez wiązania między dwoma spinami rozszczepia każdy pik na multiplet. Jądro z nn równoważnymi sąsiadami rozszczepia się na n+1n+1 linii o intensywnościach dwumianowych, czyli regułę n+1n+1. Wzór rozszczepienia ujawnia łączność (które jądra są związane), a odstęp to stała sprzężenia JJ.

Eksperyment​

Aby zamodelować to na naszym komputerze kwantowym, musimy zaimplementować hamiltonian spinowy na kubitach. W granicy słabego sprzężenia i silnego pola (różnice przesunięć chemicznych ≫\gg sprzężenia) części XXXX i YYYY sprzężenia uśredniają się do zera, pozostawiając tylko ZZZZ. Hamiltonian ma więc postać (w jednostkach częstotliwości kątowej, ℏ=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}}.

Każde jądro to jeden kubit: przesunięcie chemiczne to jednokubitowy obrót ZZ, a sprzężenie to dwukubitowe oddziaływanie ZZZZ.

Eksperyment przebiega następująco. Impuls π/2\pi/2 odchyla każdy spin do płaszczyzny poprzecznej, a spiny precesują i sprzęgają się przez czas tt. Rejestrujemy zespoloną magnetyzację poprzeczną s(t)=⟨X⟩+i⟨Y⟩s(t) = \langle X\rangle + i\langle Y\rangle zsumowaną po wszystkich spinach. Podobnie jak w powyższym eksperymencie Ramseya, ta magnetyzacja poprzeczna wyznacza zanikającą sinusoidę w funkcji czasu precesji tt. Gdy wykonamy transformatę Fouriera sygnału, otrzymamy widmo z pikami przy przesunięciu chemicznym νi\nu_i rozszczepionymi przez sprzężenie skalarne JijJ_{ij}.

uwaga

Symulujemy ewolucję cząsteczki, odwzorowując czas tt na kąty obrotu: większe tt oznacza większe obroty ZZ i ZZZZ, a nie dłuższy czy głębszy obwód. Zatem choć symulowany stan rzeczywiście ewoluuje wraz z tt (to sprawia, że swobodny zanik indukcji (FID) oscyluje), fizyczne kubity nigdy nie czekają dłużej i nie ma dekoherencji zależnej od tt. Symulowany FID nigdy więc sam nie zanika. Dla nieskończenie długiego idealnego FID linie widmowe byłyby nieskończenie ostre; nasze skończone okno próbkowania nadaje im natomiast skończoną rozdzielczość i może wprowadzać przeciek widmowy. Prawdziwe linie NMR mają skończoną szerokość wyznaczoną przez T2∗T_2^*. Dodajemy ją z powrotem, mnożąc FID przez okno e−t/T2∗e^{-t/T_2^{*}} przed transformatą Fouriera. Nazywa się to apodyzacją poszerzającą linie, którą spektroskopiści stosują w przetwarzaniu, aby ustawić szerokość linii (wymieniając rozdzielczość na stosunek sygnału do szumu). (Opcjonalna komórka w dodatku do tego modułu pokazuje alternatywę z dekompozycją Trottera, w której zanik wynika z rzeczywistej dekoherencji rosnącej wraz z głębokością obwodu.)

Układ AX​

Zacznijmy od tak zwanego układu AX, cząsteczki składającej się z dwóch sprzężonych, chemicznie odmiennych spinów. Etykiety A i X oznaczają dwa chemicznie odmienne środowiska spinowe. Litery są dowolne, ale fakt, że leżą daleko od siebie w alfabecie, ilustruje ich chemiczną odmienność.

Mapowanie i optymalizacja​

Piszemy jedną funkcję, która dla zadanych przesunięć chemicznych νi\nu_i i sprzężeń JijJ_{ij} zwraca sparametryzowany obwód przygotowania stanu. Impuls π/2\pi/2 odchyla spiny na równik, następnie obroty ZZ symulują ewolucję stanu pod wpływem członów przesunięcia chemicznego i sprzężenia JJ. Każde νi\nu_i to niewielkie przesunięcie precesji w Hz względem wybranego odniesienia, czyli nośnej układu wirującego. Tutaj ustawiamy je ręcznie; w prawdziwej cząsteczce ekranowanie elektronowe zmienia lokalne pole magnetyczne i przesuwa rezonans każdego jądra względem tego odniesienia. Modelują to jednokubitowe bramki RZR_Z, po jednej na spin, a każde νi\nu_i ustala, gdzie w widmie leży pik tego spinu. Dwukubitowa bramka ZZZZ realizuje sprzężenie JijJ_{ij}, które rozszczepia każdy pik na multiplet i wyznacza odstęp między jego liniami.

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

Wykonanie i przetwarzanie końcowe​

Obliczamy obwód na siatce czasów ewolucji, a Estimator zwraca ⟨X⟩\langle X\rangle i ⟨Y⟩\langle Y\rangle dla każdego z nich. Łączymy je w zespolony FID ⟨X⟩+i⟨Y⟩\langle X\rangle + i\langle Y\rangle, stosujemy okno apodyzacji i wykonujemy transformatę Fouriera wyniku, aby otrzymać widmo.

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

Rozszerzenie: układ AX2_2 i reguła n+1n+1​

Dodajmy teraz drugiego równoważnego sąsiada. W układzie AX2_2 jeden spin A sprzęga się, z tym samym JJ, z dwoma równoważnymi spinami X. (Pamiętaj, X to tylko etykieta dwóch równoważnych spinów; nie ma nic wspólnego z operatorami Pauliego XX. Każde poniższe sprzężenie to nadal ZZZZ.) Spin A widzi wtedy cztery konfiguracje sąsiadów (↑↑, ↑↓, ↓↑, ↓↓)(\uparrow\uparrow,\ \uparrow\downarrow,\ \downarrow\uparrow,\ \downarrow\downarrow); dwie mieszane pokrywają się, więc A rozszczepia się na tryplet 1:2:1 (n=2⇒n+1=3n=2 \Rightarrow n+1 = 3 linie). Każdy spin X widzi tylko A i pozostaje dubletem. Równoważne spiny nie rozszczepiają się nawzajem, więc nie uwzględniamy sprzężenia X–X.

Prawdziwą cząsteczką o tym wzorze jest 1,1,2-trichloroetan (Cl2_2CH–CH2_2Cl). Gdy celujemy w spiny wodoru (co jest najczęstszą formą NMR), pojedynczy proton wodoru w CH pełni rolę A, a dwa równoważne protony CH2_2 to dwa spiny X. Teraz wystarczy policzyć sąsiadów: proton CH ma dwóch sąsiadów (parę CH2_2), więc jego sygnał rozszczepia się na tryplet 1:2:1 (n=2n=2); każdy proton CH2_2 ma tylko jednego sąsiada (CH), więc sygnał CH2_2 to dublet 1:1 (n=1n=1). Zauważ, że grupa z mniejszą liczbą protonów (CH) wykazuje więcej linii; liczba linii odzwierciedla, ilu sąsiadów ma sygnał, a nie ile protonów go dzieli.

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

Poniżej symulujemy układ AX2_2 z trzema kubitami.

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

Sprawdź, czy rozumiesz​

Przeczytaj każde pytanie, zastanów się nad odpowiedzią, a następnie kliknij trójkąt, aby odsłonić rozwiązanie.

W widmie AX dwa dublety są oddzielone (od środka do środka) o 12 Hz, a dwie linie w każdym dublecie dzieli 2 Hz. Która liczba to różnica przesunięć chemicznych, a która to stała sprzężenia JJ? Gdybyś przeszedł na spektrometr o dwukrotnie silniejszym polu magnetycznym, która z nich by się zmieniła?

Odpowiedź

Odstęp 12 Hz od środka do środka to różnica przesunięć chemicznych (ν1−ν2\nu_1-\nu_2); odstęp linii 2 Hz to stała sprzężenia JJ. Przesunięcia chemiczne wynikają z ekranowania B0B_0, więc skalują się z polem. Przy dwukrotnie silniejszym polu odstęp podwaja się do 24 Hz. Sprzężenie JJ jest właściwością cząsteczki przenoszoną przez wiązania i nie zależy od pola, więc pozostaje 2 Hz. Chemicy odróżniają przesunięcie od sprzężenia, obserwując, jak rozszczepienie skaluje się z polem.

Korzystając z reguły n+1n+1, przewidź multiplet A, gdyby dodano trzeci równoważny spin X (układ AX3_3), i wyjaśnij, dlaczego dwa równoważne spiny X w AX2_2 nie rozszczepiają się nawzajem.

Odpowiedź

Przy n=3n=3 równoważnych sąsiadach A staje się kwartetem 1:3:3:1. Równoważne spiny nie rozszczepiają się nawzajem, ponieważ ich sprzężenie przesuwa odpowiednie poziomy energii o tę samą wartość, więc odstępy między tymi poziomami — czyli częstotliwości, które widzimy — się nie zmieniają.

Podsumowanie​

Użyliśmy prawdziwego komputera kwantowego, aby zbadać, jak spiny jądrowe precesują i ulegają dekoherencji w eksperymencie NMR oraz jak interpretować otrzymane widma. Warto cofnąć się o krok i przypomnieć, dlaczego to w ogóle było możliwe i dlaczego to coś więcej niż ciekawostka.

Kubit i jądro o spinie 1/2 są matematycznie tym samym obiektem: dwupoziomowymi układami kwantowymi żyjącymi na sferze Blocha, obracanymi przez impulsy rezonansowe, precesującymi wokół osi i tracącymi spójność na rzecz otoczenia. Całe słownictwo tego modułu (częstotliwość Larmora, układ wirujący, T1T_1, T2T_2 i T2∗T_2^*, prążki Ramseya, echo Hahna) w równym stopniu należy do NMR i do obliczeń kwantowych. To, co chemik nazywa przesunięciem chemicznym, inżynier kwantowy nazywa jednokubitowym obrotem ZZ; to, co chemik nazywa sprzężeniem JJ, inżynier kwantowy nazywa oddziaływaniem ZZZZ. Obie dziedziny opisywały tę samą fizykę innymi słowami.

Ten związek jest historyczny, a nie tylko pojęciowy: NMR było jedną z pierwszych platform, na których uruchomiono prawdziwe algorytmy kwantowe. Do 2001 roku cząsteczka w roztworze wykonała algorytm Shora, rozkładając na czynniki liczbę 15, przy czym każdy spin jądrowy służył jako kubit. NMR przekazało też obliczeniom kwantowym dużą część ich zestawu narzędzi sterowania. Echo spinowe, ciągi impulsów CPMG, impulsy złożone i sprzężenie dynamiczne zostały wynalezione przez spektroskopistów na dziesięciolecia, zanim kubity potrzebowały ich do walki z dekoherencją. W tym, co właśnie zrobiliśmy, jest satysfakcjonująca symetria: dziedzina, która dała początek wczesnym komputerom kwantowym, jest teraz czymś, co komputer kwantowy potrafi symulować.

Powtórzenie i pytania​

Kluczowe pojęcia​

  • Kubit to zaprojektowany układ dwupoziomowy równoważny spinowi 1/2; jego ∣0⟩/∣1⟩|0\rangle/|1\rangle, częstotliwość, bramki, T1T_1 i T2T_2 są bezpośrednimi odpowiednikami wielkości NMR.

  • Impuls rezonansowy obraca wektor Blocha; kąt odchylenia wyznacza obrót, a krzywa Rabiego/nutacji P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) identyfikuje obroty π\pi i π/2\pi/2.

  • T1T_1 to relaksacja energii (spin-sieć); T2T_2 to relaksacja fazy (spin-spin); T2∗T_2^{*} dodatkowo obejmuje odwracalną defazację statyczną, którą echo Hahna ogniskuje.

  • Cząsteczka to zbiór sprzężonych spinów: przesunięcie chemiczne wyznacza położenie każdego piku, a sprzężenie JJ rozszczepia piki na multiplety zgodnie z regułą n+1n+1, co razem koduje środowisko chemiczne i łączność.

Prawda / fałsz​

  1. P/F Częstotliwość Larmora to przerwa energetyczna między dwoma stanami spinowymi podzielona przez ℏ\hbar.

  2. P/F Impuls 90∘90^\circ (π/2\pi/2) w pełni odwraca spin w równowadze.

  3. P/F T2∗T_2^{*} jest zazwyczaj dłuższe niż T2T_2.

  4. P/F Echo Hahna ogniskuje nieodwracalną defazację spin-spin.

  5. P/F W układzie AX2_2 jądro A pojawia się jako tryplet 1:2:1.

  6. P/F Stała sprzężenia JJ podwaja się, gdy podwoisz pole magnetyczne spektrometru.

  7. P/F Usunięcie Qiskit Pulse uniemożliwia pomiar T1T_1 na obecnym sprzęcie.

Krótka odpowiedź​

  1. Wyjaśnij w dwóch zdaniach, dlaczego pojedynczy impuls π\pi jest kiepskim sposobem wykrycia błędu kąta odchylenia rzędu 5%5\% i jak łączenie impulsów to naprawia.

  2. Próbka wykazuje szerokie linie NMR. Co to mówi w kategoriach T2T_2 i jaka wielkość kubitu jest jej odpowiednikiem?

  3. Podaj odpowiednik kubitowy każdego z nich: (a) opóźnienie rekonwersji, (b) impuls 90∘90^\circ, (c) przesunięcie chemiczne.

  4. Korzystając z reguły n+1n+1, wyjaśnij, dlaczego grupa etylowa (CH3_3CH2_2–) daje tryplet 1:2:1 i kwartet 1:3:3:1, oraz wskaż, która grupa daje który multiplet.

Zadania wyzwań​

  1. Ciąg Carra-Purcella. Zmodyfikuj echo Hahna, wstawiając nn równo rozmieszczonych impulsów π\pi (ciąg w stylu CPMG) i zmierz T2T_2 w funkcji nn. Omów, dlaczego więcej impulsów ogniskujących może wydłużyć pozorne T2T_2.

  2. Wzmacnianie błędu impulsu. Zbuduj obwód, który stosuje RX(π)R_X(\pi)  n\,n razy do ∣0⟩|0\rangle, i wykreśl P1P_1 względem nn dla celowo błędnie ustawionego kąta RX(1.05π)R_X(1.05\pi). Pokaż, jak błąd staje się widoczny wraz ze wzrostem nn. Wskazówka: umieść barierę po każdym obrocie; w przeciwnym razie transpiler może połączyć powtarzane obroty w jedną równoważną bramkę.

  3. Grupa etylowa. Używając fid_circuit, zbuduj widmo protonowe fragmentu etylowego (CH3_3–CH2_2): pięć kubitów, przy czym trzy protony CH3_3 mają wspólne przesunięcie chemiczne, a dwa protony CH2_2 inne, a każda para CH3_3–CH2_2 jest sprzężona tym samym JJ (bez sprzężenia w obrębie grupy równoważnej). Przewidź wzór za pomocą reguły n+1n+1, a następnie zasymuluj go; powinieneś otrzymać kwartet 1:3:3:1 (CH2_2) i tryplet 1:2:1 (CH3_3).

Dodatek (opcjonalny): ewolucja z dekompozycją Trottera — niech dekoherencja wyznacza szerokość linii​

W sekcji Spektroskopia NMR cząsteczki ewolucja e−iHte^{-iHt} jest stosowana w jednej warstwie, której głębokość nie rośnie z tt, więc FID nie zanika sam i szerokość linii narzucamy oknem apodyzacji e−t/T2e^{-t/T_2}. Ta opcjonalna komórka przyjmuje podejście przeciwne. Dzieli ewolucję na pp kroków Trottera i umieszcza barierę po każdym kroku (aby transpiler nie mógł scalić ich z powrotem w jedną warstwę). Punkt czasowy kk używa kk kroków, więc głębokość obwodu rośnie z czasem.

Ponieważ wszystkie człony w HH komutują, jest to dokładne: na bezszumowym symulatorze daje te same ostre piki. Ale na Backend z modelem szumu lub na prawdziwym sprzęcie kumulujący się błąd bramek sam powoduje zanik FID i to ten zanik wyznacza szerokość linii. To idea T2↔T_2\leftrightarrowszerokość linii z sekcji eksperyment T2T_2, T2∗T_2^*, teraz wynikająca z rzeczywistej dekoherencji zamiast z okna przetwarzania. W ten sposób uruchamiamy zarówno układ AX (dwa dublety), jak i układ AX2_2 (tryplet 1:2:1 plus dublet).

Dwie praktyczne uwagi. Po pierwsze, kosztem jest głębokość: najgłębszy obwód AX używa ∼ ⁣47\sim\!47 bramek dwukubitowych, a wersja AX2_2 około dwa razy tyle, więc oba są znacznie bardziej zaszumione i droższe niż wersja dokładna. Po drugie, każdy układ spinowy jest nadal przesyłany jako jedno zadanie, a każdy punkt czasowy ponownie używa tego samego układu fizycznych kubitów wybranego w sekcji spektroskopii. Jeśli USE_BATCH=True, zadania te korzystają też z opcjonalnego Batch obejmującego cały notatnik.

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