Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Ядерний магнітний резонанс із Qiskit

Для цього модуля Qiskit in Classrooms студенти повинні мати робоче середовище Python із встановленими такими пакетами:

  • qiskit v2.1 або новіший
  • qiskit-ibm-runtime v0.40.1 або новіший
  • qiskit-aer v0.17 або новіший
  • scipy і matplotlib (для апроксимації та побудови графіків)
  • pylatexenc (для відтворення діаграм схем)

Щоб налаштувати та встановити наведені вище пакети, дивись посібник Встановлення Qiskit. Щоб виконувати завдання на реальних квантових комп'ютерах, студенти повинні створити обліковий запис IBM Quantum®, виконавши кроки з посібника Налаштування облікового запису IBM Cloud®.

Цей модуль протестовано, і він використовує приблизно 5 хвилин часу QPU. Це оцінка, і фактичне використання може відрізнятися.

Вступ​

Коли хімік виконує експеримент ЯМР, він розміщує зразок ядерних спінів у сильному магнітному полі, подає радіочастотний (РЧ) імпульс і вимірює сигнал, що повертається. Ця техніка ґрунтується на кількох ідеях квантової механіки частинки зі спіном 1/2 у магнітному полі. Спін має два рівні енергії, розділені енергією, що відповідає характерній ларморовій частоті. Резонансний імпульс на цій частоті перекидає спін у поперечну площину, де він прецесує та індукує сигнал, який потім згасає з двома характерними часами, T1T_1 і T2T_2.

Надпровідний кубіт — це інженерна версія того самого об'єкта: контрольована дворівнева квантова система. Його стани ∣0⟩|0\rangle і ∣1⟩|1\rangle є аналогом спіну вгору та спіну вниз, вентиль, яким ми його обертаємо, — аналогом РЧ-імпульсу, а когерентність він втрачає з власними T1T_1 і T2T_2. Це не просто умовне порівняння; деякі з найперших експериментальних квантових процесорів наприкінці 1990-х і на початку 2000-х використовували ЯМР.

У цьому модулі ти використаєш справжній квантовий комп'ютер, щоб виконати ті самі характеризаційні експерименти, що й спектроскопіст ЯМР (або квантовий інженер, який калібрує апаратне забезпечення):

  • Рабі / нутація — збудження спіну та спостереження за його обертанням. Так калібрують обертання на π\pi або π/2\pi/2.

  • T1T_1 — спін-граткова релаксація, повернення збудженого спіну до рівноваги.

  • T2T_2 — спін-спінова релаксація (дефазування), що вимірюється експериментом Ремсі та рефокусується ехо Гана.

  • Спектроскопія ЯМР (хімічний зсув і JJ-зв'язок) — симуляція невеликої молекули на кількох кубітах і зчитування її мультиплетного спектра для відновлення зв'язності за правилом n+1n+1.

Для кожної концепції ми спочатку наводимо мінімальну теорію, а потім реалізуємо експеримент у Qiskit за чотириетапним робочим процесом шаблонів Qiskit.

Теорія​

Частинки зі спіном 1/2 у магнітному полі​

Ядро зі спіном 1/2 має магнітний момент μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}, де γ\gamma — гіромагнітне відношення. У статичному полі B0B_0 вздовж zz два спінові стани розщеплюються за енергією (ефект Зеемана):

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 .

Енергетична щілина визначає ларморову (резонансну) частоту ω0=γB0\omega_0 = \gamma B_0. Магнітний момент прецесує навколо B0B_0 із цією самою частотою.

Щоб обернути спін, ми застосовуємо осцилююче РЧ-поле на частоті ω0\omega_0 (або поблизу неї). У системі відліку, що обертається з частотою збудження (обертова система відліку), резонансне збудження обертає вектор Блоха навколо осі в площині xx-yy. Наскільки далеко він обертається (кут нутації), визначається силою та тривалістю імпульсу. Найважливіші два варіанти: імпульс 90∘90^\circ перекидає спін +1/2+1/2 у поперечну площину, а імпульс 180∘180^\circ повністю обертає його до −1/2-1/2.

Переклад між жаргоном ЯМР і квантових обчислень​

Кубіт — це дворівнева система, математично ідентична спіну 1/2 у магнітному полі — тож хоча ми працюємо на квантовому процесорі, а не на ядрі, фізика та сама, що лежить в основі ЯМР. Відрізняється лише словник. Ця таблиця допоможе тобі спілкуватися і з фізичними хіміками, і зі фахівцями з квантових обчислень.

ЯМР / спін 1/2Квантовий процесор (кубіт)
Спін вгору / спін вниз∣0⟩\ket{0} / ∣1⟩\ket{1}
Щілина Зеемана ΔE=ℏω0\Delta E = \hbar\omega_0Енергія переходу кубіта ℏω01\hbar\omega_{01}
Ларморова частота ω0\omega_0Частота кубіта ω01\omega_{01}
Резонансний РЧ-імпульс (кут нутації θ\theta)Однокубітний вентиль обертання RX(θ)R_X(\theta)
Імпульс 90∘90^\circ / 180∘180^\circRX(π/2)R_X(\pi/2) / RX(π)R_X(\pi)
Спін-граткова релаксація T1T_1Час енергетичної релаксації кубіта T1T_1
Спін-спінова релаксація T2T_2Час дефазування кубіта T2T_2
НутаціяОсциляції Рабі
Спад вільної індукції (FID), ехо ГанаПослідовності Ремсі, ехо-послідовності

Усе нижче — це вправа з вимірювання правого стовпця та його прочитання як лівого.

Налаштування середовища Qiskit​

Ми протягом усього часу дотримуємося робочого процесу патернів Qiskit:

  1. Відобразити задачу на квантове коло.

  2. Оптимізувати коло для виконання (транспілювати для бекенду).

  3. Виконати за допомогою примітива IBM Quantum.

  4. Постобробити та проаналізувати.

Спочатку ми завантажуємо сервіс runtime і вибираємо найменш завантажений реальний бекенд. Якщо ти ще не зберіг свої облікові дані, дивись сторінку документації про те, як зберегти облікові дані для входу.

# 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

Використай комірку нижче, щоб перемикатися між реальним обладнанням і локальним симулятором для всього ноутбука. Запуск на обладнанні є метою модуля, але якщо виникнуть проблеми зі з'єднанням або використанням, симулятор може слугувати запасним варіантом. Установи USE_HARDWARE = True, щоб запустити на QPU, вибраному вище.

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

Експеримент: осциляції Рабі (нутація)​

Теорія​

Якщо збуджувати спін у резонансі, його вектор Блоха рівномірно обертається від полюса до полюса — він нутує. Якщо застосувати обертання на кут повороту θ\theta навколо осі xx до кубіта, що починає в стані ∣0⟩|0\rangle, імовірність знайти його в стані ∣1⟩|1\rangle дорівнює:

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

Це крива Рабі. Її перший максимум є π\pi-імпульсом (повна інверсія, імпульс 180∘180^\circ); π/2\pi/2-імпульс є чвертю оберту (імпульс 90∘90^\circ, що перекидає спін у поперечну площину). В ЯМР ця крива є способом калібрування імпульсів.

Експериментальне застереження

Буквальний експеримент Рабі змінює амплітуду або тривалість збудження в часі. Це потребує керування на рівні імпульсів, яке було вилучено в Qiskit v2.0. Натомість тут ми змінюємо кут обертання θ\theta вентиля RX(θ)R_X(\theta). Крива та ототожнення π\pi/π/2\pi/2 ідентичні; єдина відмінність у тому, що ми вибираємо кут безпосередньо замість калібрування тривалості імпульсу. На обладнанні з дробовими вентилями RX(θ)R_X(\theta) для довільного θ\theta виконується нативно.

Погляд на сфері Блоха​

Анімація нижче показує нутацію: резонансне збудження рівномірно обертає вектор Блоха навколо осі xx, переносячи його від ∣0⟩|0\rangle вниз до ∣1⟩|1\rangle і назад. Саме цей рух вимірює крива Рабі.

Відображення задачі на коло​

Ми будуємо одне параметризоване коло, RX(θ)R_X(\theta) з наступним вимірюванням, і пізніше обчислюємо його одразу для багатьох кутів.

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

Оптимізація для виконання​

Транспілюй у коло архітектури набору інструкцій (ISA) для вибраного бекенду.

rabi_isa = pm_q.run(rabi)

Виконання​

Ми змінюємо θ\theta від 00 до 4π4\pi (два повні цикли Рабі). Передача масиву значень параметрів як єдиного примітивного уніфікованого блоку (PUB) виконує їх усі в одному завданні.

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

Постобробка та аналіз​

Витягни P1P_1 для кожного кута та порівняй із 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

Перевір своє розуміння​

На виміряній тобою кривій P1P_1 вперше досягає максимуму при π\pi-імпульсі (θ=π\theta = \pi). Тепер припустімо, що пристрій розкалібровано, так що номінальний кут θ\theta на осі x насправді дає обертання на 1.05 θ1.05\,\theta.

(a) При якому номінальному куті тепер з'явиться перший максимум P1P_1? (b) Чи був би одиночний π\pi-імпульс чутливим способом виявити цю 5% помилку?

Відповідь

(a) Максимум з'являється там, де фактичне обертання дорівнює π\pi, тобто при номінальному куті θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — невеликий зсув піка вліво.

(b) Ні. Номінальний π\pi-імпульс дає фактичне обертання 1.05π1.05\pi, що дає P1=sin⁡2(1.05π/2)≈0.994P_1 = \sin^2(1.05\pi/2) \approx 0.994 замість 11 — зміна надто мала, щоб її помітити на тлі шуму вимірювання. Саме тому для точного калібрування спектроскописти ЯМР підсилюють помилку, застосовуючи обертання багато разів (наприклад, метод 360∘360^\circ або довгу послідовність імпульсів), так що мала помилка кожного імпульсу накопичується у велике, вимірюване відхилення. Ми повернемося до цієї ідеї в складних задачах.

Експеримент: T1T_1 (спін-ґратчаста релаксація)​

Теорія​

Якщо залишити збуджений спін самого, він віддає енергію довкіллю ("ґратці") і релаксує назад до теплової рівноваги. Населеність збудженого стану спадає експоненційно з часом спін-ґратчастої релаксації T1T_1:

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

Для хіміка T1T_1 задає затримку відновлення, яку потрібно витримувати між скануваннями для кількісного ЯМР, і несе інформацію про молекулярне обертання та динаміку. Для кубіта це час енергетичної релаксації, і це одне з двох головних чисел для моделювання ефектів декогерентності в квантовому комп'ютері.

Експеримент. Приготуй ∣1⟩|1\rangle за допомогою вентиля XX (π\pi-імпульс), зачекай змінну затримку τ\tau, потім виміряй. Частка, що все ще перебуває в ∣1⟩|1\rangle, відтворює спад.

Погляд на сфері Блоха​

Релаксація T1T_1 повертає збуджений стан до рівноваги, тож вектор Блоха сковзає від ∣1⟩|1\rangle назад до ∣0⟩|0\rangle вздовж осі zz.

Побудова та транспіляція сканування затримок​

Тривалості затримок мають бути конкретними значеннями (вони плануються в апаратному часі), тому ми будуємо одне коло для кожної затримки.

# 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

Виконання​

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

Аналіз — підгонка експоненти​

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

Точний час T1T_1 різниться від експерименту до експерименту та від бекенду до бекенду, але ти маєш побачити результат близько кількох сотень мікросекунд.

Перевір своє розуміння​

У кількісному ЯМР кажуть чекати близько 5 T15\,T_1 між скануваннями. Користуючись законом спаду, яка частка рівноважної намагніченості відновиться після 5 T15\,T_1?

Відповідь

Після 5 T15\,T_1 залишкове відхилення від рівноваги становить e−5≈0.0067e^{-5}\approx 0.0067, тобто відновлено близько 99.3%.

Експеримент: T2T_2, T2∗T_2^*​

Теорія​

T1T_1 вимірює втрату енергії. T2T_2 вимірює втрату фазової когерентності в поперечній площині. Ми можемо спостерігати це за допомогою експерименту Рамсі: π/2\pi/2-імпульс повертає всі спіни до екватора, і всі вони вказують в один бік. Вони прецесують з частотою Лармора, але кожен прецесує з трохи іншою частотою, тому розходяться віялом уздовж екватора, і сумарний поперечний сигнал спадає з характерним часом спаду T2∗T_2^*.

Примітка для квантових читачів: Картина розходження віялом показує один зі способів втратити когерентність: розкид визначених фаз, які усереднюються. Варто знати, що це не єдиний механізм. Когерентність може також губитися через те, що фаза стає заплутаною з довкіллям, тож вона взагалі не є визначеним значенням, яке має система. За самим спадом сигналу не можна відрізнити ці два механізми, тому картина віяла є добрим інтуїтивним уявленням. Остерігайся сприймати її як повну чи буквальну історію.

Два ефекти дефазують спін:

  • Незворотна (або фактично незворотна) декогерентність, що виникає через неконтрольовану взаємодію з іншими ступенями свободи, яка характеризується справжнім T2T_2. В ЯМР вона зазвичай походить від молекулярного руху, що модулює взаємодії між спінами. У квантових обчисленнях вона відповідає небажаним взаємодіям між кубітами та довкіллям.
  • Статичні або повільно змінні, зворотні зсуви частоти, як-от неоднорідність поля, розкид частот між кубітами та помилки калібрування. У поєднанні зі справжнім T2T_2 вони дають коротший уявний час спаду T2∗T_2^*, що спостерігається в експерименті Рамсі.

Отже, як відновити справжній T2T_2, якщо експеримент Рамсі повідомляє лише про T2∗T_2^*? Другий експеримент, який називається ехо Хана, скасовує весь шум другого типу, щоб знайти справжній T2T_2. Він починається так само, як експеримент Рамсі, з π/2\pi/2-імпульсу, потім спіни розходяться віялом, як і раніше. Через деякий час τ\tau застосовується π\pi-імпульс. Імпульс обертає назад накопичення фази, спричинене статичними зсувами частоти, тож спіни перефокусуються після другої затримки тієї ж тривалості τ\tau.

T2∗T_2^* і T2T_2 є важливими числами як у хімії, так і в квантових обчисленнях. У хімії T2∗T_2^* задає ширину лінії у стандартному експерименті ЯМР (що це означає, ти побачиш у пізнішому розділі). T2T_2 може багато розповісти про молекули, наприклад про молекулярне обертання, а зміна T2T_2 може сигналізувати, що молекула зв'язалася з чимось більшим. У квантових обчисленнях ми загалом хочемо, щоб і T2T_2, і T2∗T_2^* були якомога довшими, щоб мати більше часу для експериментів, поки декогерентність не переважить систему. Ціла родина методів, заснованих на ехо Хана, подовжує цей час, скасовуючи зворотний шум, так що коло в ідеалі обмежене справжнім T2T_2, а не T2∗T_2^*. Це називається "динамічним розв'язуванням".

Реалізація​

Коли ми реалізуємо експерименти Рамсі та ехо Хана на кубіті, зчитування трохи відрізняється від випадку ЯМР. В ЯМР можна безпосередньо спостерігати осциляційні поля, індуковані спінами, що прецесують з частотою Лармора в поперечній площині. Але з кубітом ми можемо виміряти лише його проєкцію на вісь ZZ, тобто чи він ∣0⟩\vert 0 \rangle, чи ∣1⟩\vert 1 \rangle. Тому в обох експериментах ми завершуємо додатковим π/2\pi/2-імпульсом з подальшим вимірюванням. Цей π/2\pi/2-імпульс перетворює інформацію про фазу кубіта на населеність.

Нижче ми запускаємо кожен експеримент, щоб визначити T2∗T_2^* і T2T_2.

Рамсі (T2∗T_2^{*}): два π/2\pi/2-імпульси із затримкою τ\tau​

Перший π/2\pi/2-імпульс перекидає вектор Блоха до екватора, а наступний перетворює фазу вектора Блоха на населеність. Оскільки фаза змінюється в часі, поки вектор Блоха прецесує, ми бачимо синусоїдальну картину в населеності як функції часу прецесії τ\tau. Коли кубіт втрачає фазову когерентність, ми маємо побачити спад амплітуди цієї синусоїди.

Ще кілька тонкощів для квантових читачів: У експериментах квантових обчислень осциляції, які ми бачимо, насправді не мають частоти Лармора, яка лежить у діапазоні ГГц і надто швидка, щоб ми її розрізнили. Квантові експерименти працюють у системі відліку, що обертається: системі, яка обертається разом із кубітом з (майже) частотою Лармора, тож швидка прецесія невидима, а вектор Блоха виглядає майже нерухомим. Щоб побачити інтерференційні смуги, ми навмисно задаємо систему, що обертається, з невеликою розстройкою від частоти Лармора. Тоді вектор Блоха здається таким, що прецесує з цією малою розстройкою, і ми отримуємо осцилюючий сигнал на частоті розстройки. Нижче ми не фіксуємо розстройку на одному значенні; ми вибираємо її так, щоб близько дюжини смуг вкладалося у вікно вимірювання (розмір якого ми підбираємо за часом когерентності кубіта), що полегшує вибірку осциляції без аліасингу. Ще одна складність: у Qiskit ми не маємо керування на рівні імпульсів, потрібного для зміни частоти імпульсу (яка задає частоту системи, що обертається). Натомість ми додаємо ефективну розстройку вручну, використовуючи її для задання фази другого імпульсу Рамсі. Це трохи штучно, але результат той самий, і спад, який ми спостерігаємо, все ще є справжнім T2∗T_2^*.

Погляд на сфері Блоха​

π/2\pi/2-імпульс перекидає спіни до екватора; кожен потім прецесує з трохи іншою швидкістю, і вони розходяться віялом, тож сумарний (червоний) вектор зменшується, коли вони дефазуються — цей спад є 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

Ехо Хана (T2T_2): додавання перефокусувального π\pi-імпульсу посередині​

Погляд на сфері Блоха​

Ехо Хана: спіни розходяться віялом, як і раніше, потім π\pi-імпульс перевертає їх, тож їхня подальша прецесія перефокусовує їх назад докупи (червоний вектор знову росте), відновлюючи когерентність, втрачену через статичне дефазування.

# 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

Аналіз: підгонка та порівняння двох спадів​

# 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

Тут ми бачимо, що і осциляція Рамсі, і сигнал ехо Хана спадають, як і очікувалося. Відповідні часові масштаби цих спадів — T2∗T_2^* і T2T_2 відповідно.

Перевір своє розуміння​

Зазвичай ти маєш отримати T2≳T2∗T_2 \gtrsim T_2^{*}. Поясни чому одним реченням і скажи, який фізичний ефект усуває π\pi-імпульс ехо Хана, а який не може усунути.

Відповідь

T2∗T_2^{*} включає і незворотне дефазування, і зворотний статичний розкид, тому спадає швидше; π\pi-імпульс ехо перефокусовує статичне, зворотне дефазування (неоднорідність поля / фіксовані зсуви частоти), залишаючи лише незворотне спін-спінове дефазування, яке і є справжнім T2T_2. Ехо не може скасувати флуктуації, що змінюються між двома половинами послідовності.

ЯМР-спектроскопія молекули: хімічний зсув і JJ-взаємодія​

Від одного спіна до молекули​

Досі експерименти характеризували один ізольований спін. Реальний хімічний ЯМР зчитує молекулу (багато зв'язаних спінів одночасно) і за отриманим спектром робить висновки про особливості будови молекули. Це може розповісти про два ефекти:

  • Хімічний зсув. Кожне хімічно відмінне ядро прецесує з трохи іншою частотою νi\nu_i, бо його локальні електрони екранують прикладене поле по-різному. Тому положення піків ідентифікують хімічні оточення (наприклад, протон CH3_3 порівняно з ароматичним).

  • Скалярна JJ-взаємодія. Взаємодія через зв'язки між двома спінами розщеплює кожен пік на мультиплет. Ядро з nn еквівалентними сусідами розщеплюється на n+1n+1 ліній з біноміальними інтенсивностями, це правило n+1n+1. Картина розщеплення виявляє зв'язність (які ядра сполучені), а відстань є константою взаємодії JJ.

Експеримент​

Щоб змоделювати це на нашому квантовому комп'ютері, потрібно реалізувати спіновий гамільтоніан на кубітах. У границі слабкої взаємодії та сильного поля (різниці хімічних зсувів ≫\gg взаємодій) частини XXXX і YYYY взаємодії усереднюються, залишаючи лише ZZZZ. Отже, гамільтоніан такий (в одиницях кутової частоти, ℏ=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}}.

Кожне ядро є одним кубітом: хімічний зсув є одно-кубітним обертанням ZZ, а взаємодія є дво-кубітною взаємодією ZZZZ.

Експеримент проходить так. π/2\pi/2-імпульс перекидає кожен спін у поперечну площину, і спіни прецесують та взаємодіють протягом часу tt. Ми записуємо комплексну поперечну намагніченість s(t)=⟨X⟩+i⟨Y⟩s(t) = \langle X\rangle + i\langle Y\rangle, просумовану за всіма спінами. Як і в експерименті Рамсі вище, ця поперечна намагніченість відтворює згасаючу синусоїду як функцію часу прецесії tt. Коли ми беремо перетворення Фур'є сигналу, ми отримуємо спектр із піками при хімічному зсуві νi\nu_i, розщепленими скалярною взаємодією JijJ_{ij}.

примітка

Ми симулюємо еволюцію молекули, відображаючи час tt на кути обертання: більший tt означає більші обертання ZZ і ZZZZ, а не довше чи глибше коло. Тож хоча симульований стан справді розвивається з tt (саме це змушує спад вільної індукції (FID) осцилювати), фізичні кубіти ніколи не простоюють довше, і немає декогерентності, що залежить від tt. Тому симульований FID ніколи не спадає сам по собі. Для нескінченно довгого ідеального FID спектральні лінії були б нескінченно гострими; натомість наше скінченне вікно вибірки дає їм скінченну роздільну здатність і може вносити спектральне витікання. Реальні лінії ЯМР мають скінченну ширину, яку задає T2∗T_2^*. Ми додаємо її назад, множачи FID на вікно e−t/T2∗e^{-t/T_2^{*}} перед перетворенням Фур'є. Це називається апodізацією розширення ліній, яку спектроскописти застосовують під час обробки, щоб задати ширину лінії (обмінюючи роздільну здатність на співвідношення сигнал/шум). (Необов'язкова комірка в додатку до цього модуля показує тротеризований альтернативний варіант, у якому спад натомість походить від реальної декогерентності, що росте з глибиною кола.)

Система AX​

Почнімо з так званої системи AX, молекули з двох зв'язаних, хімічно відмінних спінів. Мітки A і X позначають два хімічно відмінні спінові оточення. Літери довільні, але те, що вони далеко одна від одної в алфавіті, ілюструє їхню хімічну несхожість.

Відображення та оптимізація​

Ми пишемо одну функцію, яка за заданих зсувів хімічних зсувів νi\nu_i і взаємодій JijJ_{ij} повертає параметризоване коло приготування стану. π/2\pi/2-імпульс перекидає спіни до екватора, потім обертання ZZ симулюють еволюцію стану під дією членів хімічного зсуву та JJ-взаємодії. Кожне νi\nu_i є малим зсувом прецесії в Гц відносно вибраного опорного значення, або несучої системи, що обертається. Тут ми задаємо його вручну; у реальній молекулі екранування електронами змінює локальне магнітне поле та зсуває резонанс кожного ядра відносно цього опорного значення. Це моделюється одно-кубітними вентилями RZR_Z, по одному на спін, і кожне νi\nu_i фіксує, де в спектрі розташований пік цього спіна. Дво-кубітний вентиль ZZZZ застосовує взаємодію JijJ_{ij}, яка розщеплює кожен пік на мультиплет і задає відстань між його лініями.

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

Виконання та постобробка​

Ми обчислюємо коло на сітці часів еволюції, і Estimator повертає ⟨X⟩\langle X\rangle та ⟨Y⟩\langle Y\rangle для кожного. Ми поєднуємо їх у комплексний FID ⟨X⟩+i⟨Y⟩\langle X\rangle + i\langle Y\rangle, застосовуємо вікно апodізації та виконуємо перетворення Фур'є результату, щоб отримати спектр.

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

Розширення: система AX2_2 і правило n+1n+1​

Тепер додамо другого еквівалентного сусіда. У системі AX2_2 один спін A взаємодіє з однаковим JJ з двома еквівалентними спінами X. (Пам'ятай, X є просто міткою для двох еквівалентних спінів; вона не має нічого спільного з операторами Паулі XX. Кожна взаємодія нижче все одно є ZZZZ.) Тоді спін A бачить чотири конфігурації сусідів (↑↑, ↑↓, ↓↑, ↓↓)(\uparrow\uparrow,\ \uparrow\downarrow,\ \downarrow\uparrow,\ \downarrow\downarrow); дві змішані збігаються, тож A розщеплюється на триплет 1:2:1 (n=2⇒n+1=3n=2 \Rightarrow n+1 = 3 лінії). Кожен спін X бачить лише A і залишається дублетом. Еквівалентні спіни не розщеплюють один одного, тож ми не включаємо взаємодію X–X.

Реальна молекула з такою картиною — 1,1,2-трихлоретан (Cl2_2CH–CH2_2Cl). Якщо орієнтуватися на спіни водню (що є найпоширенішою формою ЯМР), єдиний протон водню в CH відіграє роль A, а два еквівалентні протони CH2_2 є двома спінами X. Тепер просто порахуй сусідів: протон CH має двох сусідів (пару CH2_2), тож його сигнал розщеплюється на триплет 1:2:1 (n=2n=2); кожен протон CH2_2 має лише одного сусіда (CH), тож сигнал CH2_2 є дублетом 1:1 (n=1n=1). Зауваж, що група з меншою кількістю протонів (CH) показує більше ліній; кількість ліній відображає, скільки сусідів має сигнал, а не скільки протонів його ділять.

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

Нижче ми симулюємо систему AX2_2 з трьома кубітами.

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

Перевір своє розуміння​

Прочитай кожне питання, подумай над відповіддю, потім натисни трикутник, щоб показати розв'язок.

У спектрі AX два дублети розділені (від центра до центра) на 12 Гц, а дві лінії всередині кожного дублета розташовані на відстані 2 Гц. Яке число є різницею хімічних зсувів, а яке — константою взаємодії JJ? Якби ти перейшов на спектрометр з удвічі сильнішим магнітним полем, яке з них змінилося б?

Відповідь

Відстань 12 Гц від центра до центра є різницею хімічних зсувів (ν1−ν2\nu_1-\nu_2); відстань між лініями 2 Гц є константою взаємодії JJ. Хімічні зсуви виникають від екранування B0B_0, тому масштабуються з полем. При вдвічі сильнішому полі відстань подвоюється до 24 Гц. Взаємодія JJ є властивістю молекули через зв'язки й не залежить від поля, тож залишається 2 Гц. Хіміки відрізняють зсув від взаємодії, спостерігаючи, як розщеплення масштабується з полем.

Користуючись правилом n+1n+1, передбач мультиплет A, якби додали третій еквівалентний спін X (система AX3_3), і поясни, чому два еквівалентні спіни X в AX2_2 не розщеплюють один одного.

Відповідь

При n=3n=3 еквівалентних сусідах A стає квартетом 1:3:3:1. Еквівалентні спіни не розщеплюють один одного, бо їхня взаємодія зсуває відповідні енергетичні рівні на однакову величину, тож проміжки між цими рівнями — частоти, які ми бачимо — не змінюються.

Висновок​

Ми використали справжній квантовий комп'ютер, щоб дослідити, як ядерні спіни прецесують і декогерують в експерименті ЯМР, і як інтерпретувати отримані спектри. Варто озирнутися й переглянути, чому це взагалі було можливим і чому це більше, ніж цікавинка.

Кубіт і ядро зі спіном 1/2 математично є одним і тим самим об'єктом: дворівневими квантовими системами на сфері Блоха, що обертаються резонансними імпульсами, прецесують навколо осі та втрачають когерентність через оточення. Увесь словник цього модуля (частота Лармора, система відліку, що обертається, T1T_1, T2T_2 і T2∗T_2^*, смуги Рамсі, ехо Хана) однаково належить ЯМР і квантовим обчисленням. Те, що хімік називає хімічним зсувом, квантовий інженер називає одно-кубітним обертанням ZZ; те, що хімік називає JJ-взаємодією, квантовий інженер називає взаємодією ZZZZ. Ці дві галузі описували ту саму фізику різними словами.

Цей зв'язок історичний, а не лише концептуальний: ЯМР був однією з перших платформ, що виконали реальні квантові алгоритми. До 2001 року молекула в розчині виконала алгоритм Шора для розкладу числа 15, де кожен ядерний спін слугував кубітом. ЯМР також передав квантовим обчисленням значну частину їхнього інструментарію керування. Спінове ехо, послідовності імпульсів CPMG, складені імпульси та динамічне розв'язування були винайдені спектроскопістами за десятиліття до того, як кубітам вони знадобилися для боротьби з декогерентністю. У тому, що ми щойно зробили, є приємна симетрія: галузь, що породила ранні квантові комп'ютери, тепер є чимось, що квантовий комп'ютер може симулювати.

Огляд і запитання​

Ключові поняття​

  • Кубіт є інженерною дворівневою системою, еквівалентною спіну 1/2; його ∣0⟩/∣1⟩|0\rangle/|1\rangle, частота, вентилі, T1T_1 і T2T_2 є прямими аналогами величин ЯМР.

  • Резонансний імпульс обертає вектор Блоха; кут повороту задає обертання, а крива Рабі/нутації P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) ідентифікує обертання на π\pi і π/2\pi/2.

  • T1T_1 є енергетичною (спін-ґратчастою) релаксацією; T2T_2 є фазовою (спін-спіновою) релаксацією; T2∗T_2^{*} додатково включає зворотне статичне дефазування, яке перефокусовує ехо Хана.

  • Молекула є набором зв'язаних спінів: хімічний зсув задає положення кожного піка, а JJ-взаємодія розщеплює піки на мультиплети за правилом n+1n+1, і разом вони кодують хімічне оточення та зв'язність.

Правда / неправда​

  1. П/Н Частота Лармора є енергетичною щілиною між двома спіновими станами, поділеною на ℏ\hbar.

  2. П/Н Імпульс 90∘90^\circ (π/2\pi/2) повністю інвертує рівноважний спін.

  3. П/Н T2∗T_2^{*} зазвичай довший за T2T_2.

  4. П/Н Ехо Хана перефокусовує незворотне спін-спінове дефазування.

  5. П/Н У системі AX2_2 ядро A проявляється як триплет 1:2:1.

  6. П/Н Константа взаємодії JJ подвоюється, коли подвоїти магнітне поле спектрометра.

  7. П/Н Вилучення Qiskit Pulse робить неможливим вимірювання T1T_1 на сучасному обладнанні.

Коротка відповідь​

  1. Поясни в двох реченнях, чому одиночний π\pi-імпульс є поганим способом виявити 5%5\% помилку кута повороту, і як ланцюжок імпульсів це виправляє.

  2. Зразок показує широкі лінії ЯМР. Що це говорить у термінах T2T_2 і яка величина кубіта є аналогом?

  3. Назви аналог для кубіта кожного з таких: (a) затримка відновлення, (b) імпульс 90∘90^\circ, (c) хімічний зсув.

  4. Користуючись правилом n+1n+1, поясни, чому етильна група (CH3_3CH2_2–) показує триплет 1:2:1 і квартет 1:3:3:1, і визнач, яка група дає який мультиплет.

Складні задачі​

  1. Послідовність Карра–Парселла. Зміни ехо Хана так, щоб вставити nn рівновіддалених π\pi-імпульсів (послідовність у стилі CPMG), і виміряй T2T_2 як функцію від nn. Поясни, чому більша кількість рефокусувальних імпульсів може подовжити удаване T2T_2.

  2. Підсилення похибки імпульсу. Побудуй схему, яка застосовує RX(π)R_X(\pi)  n\,n разів до ∣0⟩|0\rangle, і побудуй графік P1P_1 залежно від nn для навмисно неправильно заданого кута RX(1.05π)R_X(1.05\pi). Покажи, як похибка стає помітною зі зростанням nn. Підказка: постав бар'єр після кожного обертання; інакше транспайлер може об'єднати повторювані обертання в один еквівалентний вентиль.

  3. Етильна група. Використовуючи fid_circuit, побудуй протонний спектр етильного фрагмента (CH3_3–CH2_2): п'ять кубітів, де три протони CH3_3 мають спільний хімічний зсув, а два протони CH2_2 — інший, і кожна пара CH3_3–CH2_2 зв'язана тією самою JJ (без зв'язку всередині еквівалентної групи). Передбач картину за правилом n+1n+1, потім змоделюй її; ти маєш отримати квартет 1:3:3:1 (CH2_2) і триплет 1:2:1 (CH3_3).

Додаток (необов'язково): тротеризована еволюція — нехай декогеренція задає ширину лінії​

У розділі ЯМР-спектроскопія молекули еволюція e−iHte^{-iHt} застосовується одним шаром, глибина якого не зростає з tt, тому FID сам по собі не загасає, і ми задаємо ширину лінії за допомогою апodизаційного вікна e−t/T2e^{-t/T_2}. Ця необов'язкова комірка застосовує протилежний підхід. Вона розбиває еволюцію на pp кроків Троттера і ставить бар'єр після кожного кроку (щоб транспайлер не міг злити їх назад в один шар). Часова точка kk використовує kk кроків, тож глибина схеми зростає з часом.

Оскільки всі члени в HH комутують, це точно: на безшумному симуляторі виходять ті самі гострі піки. Але на бекенді з моделлю шуму або на реальному обладнанні накопичення похибок вентилів змушує FID загасати самостійно, і саме це загасання задає ширину лінії. Це ідея зв'язку T2↔T_2\leftrightarrowширина лінії з розділу експеримент T2T_2, T2∗T_2^*, тепер вона виникає з реальної декогеренції, а не з вікна обробки. Ми запускаємо таким способом і систему AX (два дублети), і систему AX2_2 (триплет 1:2:1 плюс дублет).

Дві практичні примітки. По-перше, ціною є глибина: найглибша схема AX використовує ∼ ⁣47\sim\!47 дворівневих кубітових вентилів, а версія AX2_2 — приблизно вдвічі більше, тому обидві значно шумніші й дорожчі за точну версію. По-друге, кожна спінова система все одно подається як одне завдання, і кожна часова точка повторно використовує те саме розташування фізичних кубітів, вибране в розділі спектроскопії. Якщо USE_BATCH=True, ці завдання також використовують необов'язковий 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()