انتقل إلى المحتوى الرئيسي
هذا المشروع في مرحلة تجريبية. وجدت خطأ؟ افتح مشكلة على 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. هذا تقدير، وقد يختلف استخدامك الفعلي.

المقدمة​

عندما يجري الكيميائي تجربة رنين مغناطيسي نووي، فإنه يضع عينة من اللفات النووية في مجال مغناطيسي قوي، ويطبّق نبضة ترددات راديوية (RF)، ثم يقيس الإشارة العائدة. تقوم هذه التقنية على بضع أفكار من ميكانيكا الكم لجسيم ذي لف 1/2 في مجال مغناطيسي. للّف مستويان للطاقة يفصل بينهما فرق طاقة يقابل تردد لارمور المميِّز. تُميل نبضة رنينية عند ذلك التردد اللف إلى المستوى المستعرض، حيث يتقدّم (precess) ويُحدث إشارة تخبو بعد ذلك بزمنين مميّزين، هما T1T_1 وT2T_2.

الكيوبت فائق التوصيل هو نسخة مهندَسة من الكائن نفسه: نظام كمّي ثنائي المستوى يمكن التحكم فيه. حالتاه ∣0⟩|0\rangle و∣1⟩|1\rangle نظيرتان للف الأعلى والف الأسفل، والبوابة التي نطبّقها لتدويره نظيرة لنبضة RF، وهو يفقد ترابطه بزمنيه T1T_1 وT2T_2 الخاصين به. وهذه ليست مجرد مقارنة تقريبية؛ فبعض أقدم المعالجات الكمّية التجريبية في أواخر التسعينيات وأوائل الألفية استخدمت الرنين المغناطيسي النووي.

في هذه الوحدة ستستخدم حاسوبًا كمّيًا حقيقيًا لتشغيل تجارب التوصيف نفسها التي يجريها أخصائي الأطياف بالرنين المغناطيسي النووي (أو مهندس كمّي يعاير العتاد):

  • راబي / التمايل (nutation) — تحريك اللف ومراقبته وهو يدور. هكذا تعاير دوران π\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. ويتقدّم العزم المغناطيسي (precesses) حول 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}
نبضة RF رنينية (زاوية القلب θ\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
التمايل (Nutation)تذبذبات رابي
اضمحلال الحث الحر (FID)، صدى هانتسلسلات رامزي والصدى

كل ما يلي هو تمرين في قياس العمود الأيمن وقراءته كأنه العمود الأيسر.

إعداد بيئة Qiskit​

نتبع مسار عمل أنماط Qiskit طوال الوقت:

  1. ربط المسألة بدارة كمّية.

  2. تحسين الدارة للتنفيذ (التحويل إلى الـ Backend).

  3. تنفيذ باستخدام أولية من IBM Quantum.

  4. معالجة لاحقة وتحليل.

نحمّل أولًا خدمة وقت التشغيل ونختار الـ Backend الحقيقي الأقل انشغالًا. إذا لم تكن قد حفظت بيانات اعتمادك بعد، فراجع صفحة الوثائق حول كيفية حفظ بيانات اعتماد تسجيل الدخول.

# 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

حسّن للتنفيذ​

حوّل الدائرة (transpile) إلى دائرة بنية مجموعة التعليمات (ISA) للـ Backend المختار.

rabi_isa = pm_q.run(rabi)

نفّذ​

نمسح θ\theta من 00 إلى 4π4\pi (دورتا Rabi كاملتان). تمرير مصفوفة من قيم المعاملات كـ Primitive Unified Bloc (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.

(أ) عند أي زاوية اسمية ستظهر الآن أول قيمة قصوى لـ P1P_1؟ (ب) هل تطبيق نبضة π\pi واحدة طريقة حساسة لاكتشاف هذا الخطأ البالغ 5%؟

الإجابة

(أ) تظهر القيمة القصوى عندما يساوي الدوران الفعلي π\pi، أي عند الزاوية الاسمية θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — إزاحة صغيرة للقمة نحو اليسار.

(ب) لا. نبضة π\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 — تغيّر أصغر من أن يُرى وسط ضجيج القياس. لهذا السبب، ولكي تُعايِر بدقة، يضخّم أخصائيو مطيافية NMR الخطأ بتطبيق الدوران مرات كثيرة (مثل طريقة 360∘360^\circ أو سلسلة نبضات طويلة)، فيتراكم الخطأ الصغير في كل نبضة إلى انحراف كبير يمكن قياسه. سنعود إلى هذه الفكرة في مسائل التحدي.

تجربة: T1T_1 (استرخاء السبين-الشبكة)​

النظرية​

عندما يُترك السبين المثار وحده، يفقد طاقته لمحيطه ("الشبكة") ويسترخي عائدًا نحو الاتزان الحراري. يتناقص تعداد الحالة المثارة أسيًّا مع زمن استرخاء السبين-الشبكة T1T_1:

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

بالنسبة للكيميائي، يحدد T1T_1 زمن التأخير بين التكرارات الذي يجب أن تنتظره بين المسحات في NMR الكمي، كما يعكس تدحرج الجزيئات وديناميكيتها. وفي الكيوبت هو زمن استرخاء الطاقة، وهو أحد الرقمين الرئيسيين المستخدمين لنمذجة تأثيرات فقدان التماسك في الحاسوب الكمي.

التجربة. حضّر ∣1⟩|1\rangle ببوابة XX (نبضة π\pi)، ثم انتظر تأخيرًا متغيرًا τ\tau، ثم قس. الجزء الذي لا يزال في ∣1⟩|1\rangle يرسم منحنى التناقص.

منظور كرة Bloch​

يعيد استرخاء T1T_1 الحالة المثارة إلى الاتزان، فينزلق متجه Bloch من ∣1⟩|1\rangle عائدًا إلى ∣0⟩|0\rangle على طول المحور zz.

ابنِ وحوّل (transpile) مسح التأخير​

يجب أن تكون مدد التأخير قيمًا محددة (لأنها تُجدوَل بزمن العتاد)، لذلك نبني دائرة واحدة لكل تأخير.

# 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 الدقيق من تجربة لأخرى ومن Backend لآخر، لكن يجب أن تحصل على نتيجة في حدود بضع مئات من الميكروثواني.

تحقق من فهمك​

في NMR الكمي يُطلب منك الانتظار نحو 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 فيقيس فقدان تماسك الطور في المستوى المستعرض. يمكننا رصد ذلك بـ تجربة Ramsey: نبضة π/2\pi/2 تُدير كل السبينات إلى خط الاستواء، وكلها تشير في الاتجاه نفسه. تقوم هذه السبينات بحركة Larmor الدورانية، لكن كلًا منها يدور بتردد مختلف قليلًا، فتنتشر على خط الاستواء كالمروحة وتتناقص الإشارة المستعرضة الصافية، بزمن تناقص مميز هو T2∗T_2^*.

ملاحظة جانبية لقراء الكم: صورة الانتشار المروحي تُظهر طريقة واحدة لفقدان التماسك: توزّع لأطوار محددة يلغي بعضها بعضًا في المتوسط. من المهم أن تعرف أنها ليست الآلية الوحيدة. فقد يُفقد التماسك أيضًا لأن الطور يصبح متشابكًا مع البيئة، فلا يعود قيمة محددة يملكها النظام أصلًا. لا يمكنك التمييز بين الآليتين بمجرد مراقبة تناقص الإشارة، لذا فصورة الانتشار المروحي حدس جيد. لكن احرص على ألا تقرأها على أنها القصة الكاملة أو الحرفية.

هناك تأثيران يسببان فقدان الطور في السبين:

  • فقدان تماسك غير عكوس (أو غير عكوس عمليًا) ينشأ من اقتران غير متحكَّم به مع درجات حرية أخرى، ويتميز بـ T2T_2 الحقيقي. في NMR ينتج هذا عادةً عن حركة الجزيئات التي تعدّل الاقترانات بين السبينات. وفي الحوسبة الكمية يقابل الاقترانات غير المرغوبة بين الكيوبتات والبيئة.
  • إزاحات تردد ثابتة أو بطيئة التغير وعكوسة، مثل عدم تجانس المجال، وتباين التردد بين كيوبت وآخر، وأخطاء المعايرة. وبالاجتماع مع T2T_2 الحقيقي، تنتج زمن تناقص ظاهريًا أقصر T2∗T_2^* يُلاحَظ في تجربة Ramsey.

إذًا كيف نستعيد T2T_2 الحقيقي إذا كانت تجربة Ramsey تخبرنا فقط عن T2∗T_2^*؟ التجربة الثانية، وتسمى صدى Hahn، تلغي كل الضجيج من النوع الثاني لتجد T2T_2 الحقيقي. تبدأ مثل تجربة Ramsey، بنبضة π/2\pi/2، ثم تنتشر السبينات كما سبق. وبعد زمن τ\tau تُطبَّق نبضة π\pi. تعكس هذه النبضة تراكم الطور الناتج عن إزاحات التردد الثابتة، فتعاود السبينات التجمع بعد تأخير ثانٍ بالمدة نفسها τ\tau.

كلٌّ من T2∗T_2^* و T2T_2 رقمان مهمان في سياق الكيمياء وفي سياق الحوسبة الكمية. في سياق الكيمياء، يحدد T2∗T_2^* عرض الخط في تجربة NMR قياسية (ستفهم معنى هذا في قسم لاحق). ويمكن لـ T2T_2 أن يخبرك الكثير عن الجزيئات، مثل تدحرج الجزيئات، وقد يدل تغيّر T2T_2 على أن جزيئًا ارتبط بشيء أكبر. وفي سياق الحوسبة الكمية، نريد عمومًا أن يكون T2T_2 و T2∗T_2^* أطول ما يمكن، ليكون لدينا وقت أطول لإجراء التجارب قبل أن يطغى فقدان التماسك على النظام. وهناك عائلة كاملة من التقنيات المبنية على صدى Hahn تمدّد هذا الزمن بإلغاء الضجيج العكوس، بحيث تكون الدائرة محدودة مثاليًا بـ T2T_2 الحقيقي لا بـ T2∗T_2^*. تسمى هذه "الفصل الديناميكي" (dynamical decoupling).

التنفيذ​

عندما ننفذ تجربتي Ramsey وصدى Hahn على كيوبت، تختلف القراءة قليلًا عن حالة NMR. في NMR يمكنك رصد المجالات المتذبذبة مباشرةً، المستحثة من دوران السبينات بتردد Larmor في المستوى المستعرض. أما مع الكيوبت فلا يمكننا إلا قياس إسقاطه على المحور ZZ، أي هل هو ∣0⟩\vert 0 \rangle أم ∣1⟩\vert 1 \rangle. لذلك ننهي كلتا التجربتين بنبضة π/2\pi/2 إضافية يتبعها قياس. تحوّل هذه النبضة π/2\pi/2 معلومات طور الكيوبت إلى تعداد.

فيما يلي نشغّل كل تجربة لتحديد T2∗T_2^* و T2T_2.

Ramsey (T2∗T_2^{*}): نبضتا π/2\pi/2 بينهما زمن تأخير τ\tau​

تميل النبضة الأولى π/2\pi/2 بمتجه Bloch إلى خط الاستواء، وتحوّل الثانية طور متجه Bloch إلى تعداد. ولأن الطور يتغير مع الزمن بينما يدور متجه Bloch، نرى نمطًا جيبيًا في التعداد كدالة لزمن الدوران τ\tau. وعندما يفقد الكيوبت تماسك الطور، يجب أن نرى سعة هذا المنحنى الجيبي تتناقص.

بعض الدقائق الإضافية لقراء الكم: في تجارب الحوسبة الكمية، التذبذبات التي نراها ليست فعليًا بتردد Larmor، الذي يقع في نطاق GHz وأسرع بكثير من أن نتمكن من تمييزه. تعمل التجارب الكمية في الإطار الدوار: إطار يدور مع الكيوبت بتردد (قريب من) تردد Larmor، فيصبح الدوران السريع غير مرئي ويبدو متجه Bloch شبه ساكن. ولكي نرى الأهداب، نضبط الإطار الدوار عمدًا بحيث ينحرف قليلًا عن تردد Larmor. عندها يبدو متجه Bloch كأنه يدور بهذا الانحراف الصغير، ونحصل على إشارة متذبذبة بتردد الانحراف. فيما يلي، لا نثبّت الانحراف عند قيمة واحدة؛ بل نختاره بحيث تغطي نحو اثني عشر هدبًا نافذة القياس (التي نحدد حجمها وفق زمن تماسك الكيوبت)، مما يبقي التذبذب سهل الاعتيان دون تداخل طيفي (aliasing). ثمة تعقيد آخر: في Qiskit لا نملك التحكم على مستوى النبضات اللازم لتغيير تردد النبضة (وهو ما يحدد تردد الإطار الدوار). بدلًا من ذلك، نضيف انحرافًا فعّالًا يدويًا، ونستخدمه لضبط طور نبضة Ramsey الثانية. الأمر مصطنع بعض الشيء، لكن النتيجة هي نفسها، ويبقى التناقص الذي نلاحظه هو T2∗T_2^* الحقيقي.

منظور كرة Bloch​

تميل نبضة π/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

صدى Hahn (T2T_2): أضف نبضة π\pi لإعادة التجميع في المنتصف​

منظور كرة Bloch​

صدى Hahn: تنتشر السبينات كما سبق، ثم تقلبها نبضة π\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

نرى هنا كلًّا من تذبذب Ramsey وإشارة صدى Hahn تتناقص، كما هو متوقع. والمقياسان الزمنيان المرتبطان بهذين التناقصين هما T2∗T_2^* و T2T_2 على التوالي.

تحقق من فهمك​

يجب أن تجد عادةً أن T2≳T2∗T_2 \gtrsim T_2^{*}. اشرح السبب في جملة واحدة، وبيّن أي تأثير فيزيائي تزيله نبضة π\pi في صدى Hahn، وما الذي لا تستطيع إزالته.

الإجابة

يشمل T2∗T_2^{*} كلًّا من فقدان الطور غير العكوس و الانتشار الساكن العكوس، لذلك يتناقص أسرع؛ أما نبضة π\pi في الصدى فتعيد تجميع فقدان الطور الساكن العكوس (عدم تجانس المجال / إزاحات التردد الثابتة)، ولا يبقى سوى فقدان الطور غير العكوس بين السبينات، وهو T2T_2 الحقيقي. ولا يستطيع الصدى التراجع عن التقلبات التي تتغير بين نصفَي التسلسل.

مطيافية NMR لجزيء: الإزاحة الكيميائية واقتران JJ​

من سبين واحد إلى جزيء​

وصّفت التجارب السابقة سبينًا واحدًا معزولًا. أما NMR الكيميائي الحقيقي فيقرأ جزيئًا (سبينات كثيرة مقترنة معًا في آن واحد) ويستخدم الطيف الناتج لاستنتاج خصائص بنية الجزيء. وهذا يمكن أن يخبرنا عن تأثيرين:

  • الإزاحة الكيميائية. تدور كل نواة مختلفة كيميائيًا بتردد مختلف قليلًا νi\nu_i، لأن إلكتروناتها المحلية تحجب المجال المطبَّق بمقادير مختلفة. لذلك تحدد مواضع القمم البيئات الكيميائية (بروتون CH3_3 مقابل بروتون عطري، مثلًا).

  • اقتران JJ العددي. تفاعل عبر الروابط بين سبينين يشطر كل قمة إلى قمم متعددة (multiplet). النواة التي لها 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 مجموعةً على كل السبينات. كما في تجربة Ramsey أعلاه، ترسم هذه المغنطة المستعرضة موجة جيبية متناقصة كدالة لزمن الدوران tt. وعندما نأخذ تحويل Fourier للإشارة، نحصل على طيف بقمم عند الإزاحة الكيميائية νi\nu_i منشطرة باقتران JijJ_{ij} العددي.

ملاحظة

نحاكي تطور الجزيء بربط الزمن tt بـ زوايا الدوران: كلما كبر tt كبرت دورانات ZZ و ZZZZ، وليس بأن تصبح الدائرة أطول أو أعمق. لذلك، رغم أن الحالة المحاكاة تتقدم فعلًا مع tt (وهذا ما يجعل تناقص الحث الحر (FID) يتذبذب)، فإن الكيوبتات الفيزيائية لا تبقى خاملة وقتًا أطول ولا يوجد فقدان تماسك يعتمد على tt. لذلك لا يتناقص FID المحاكى من تلقاء نفسه. فمع FID مثالي طويل بلا نهاية ستكون الخطوط الطيفية حادة بلا نهاية؛ أما نافذة الاعتيان المحدودة لدينا فتمنحها دقة محدودة وقد تُدخل تسربًا طيفيًا. الخطوط الحقيقية في NMR لها عرض محدود يحدده T2∗T_2^*. ونعيده بضرب FID بنافذة e−t/T2∗e^{-t/T_2^{*}} قبل تحويل Fourier. يسمى هذا تنعيم توسيع الخط (line-broadening apodization)، ويطبّقه أخصائيو المطيافية أثناء المعالجة لضبط عرض الخط (مقايضين الدقة بنسبة الإشارة إلى الضجيج). (توجد خلية اختيارية في ملحق هذه الوحدة تعرض بديلًا بتقنية Trotter، حيث ينشأ التناقص بدلًا من ذلك من فقدان تماسك حقيقي يزداد مع عمق الدائرة.)

نظام 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، ونطبق نافذة التنعيم، ثم نجري تحويل Fourier للنتيجة للحصول على الطيف.

# --- 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 مجرد تسمية للسبينين المتكافئين؛ ولا علاقة لها بمؤثرات Pauli 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). عند استهداف سبينات الهيدروجين (وهو الشكل الأكثر شيوعًا من NMR)، يلعب بروتون الهيدروجين المنفرد في 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 Hz، ويفصل بين الخطين داخل كل ثنائية 2 Hz. أي الرقمين هو فرق الإزاحة الكيميائية وأيهما ثابت الاقتران JJ؟ ولو انتقلت إلى مطياف بمجال مغناطيسي مضاعف، فأيهما سيتغير؟

الإجابة

الفصل بين المركزين البالغ 12 Hz هو فرق الإزاحة الكيميائية (ν1−ν2\nu_1-\nu_2)؛ وتباعد الخطين البالغ 2 Hz هو ثابت الاقتران JJ. تنشأ الإزاحات الكيميائية من حجب B0B_0، لذا تتناسب مع المجال. عند مضاعفة المجال يتضاعف الفصل إلى 24 Hz. أما الاقتران JJ فخاصية جزيئية عبر الروابط ولا يعتمد على المجال، فيبقى 2 Hz. يميّز الكيميائيون الإزاحة عن الاقتران بمراقبة كيف يتغير الانشطار مع المجال.

باستخدام قاعدة n+1n+1، توقّع شكل القمم المتعددة لـ A لو أُضيف سبين X مكافئ ثالث (نظام AX3_3)، واشرح لماذا لا يشطر السبينان X المتكافئان في AX2_2 أحدهما الآخر.

الإجابة

مع n=3n=3 جيران متكافئين، يصبح A رباعية 1:3:3:1. السبينات المتكافئة لا تشطر بعضها بعضًا لأن اقترانها يزيح مستويات الطاقة المعنية كلها بالمقدار نفسه، فلا تتغير الفجوات بين تلك المستويات — أي الترددات التي نراها.

الخلاصة​

استخدمنا حاسوبًا كميًا حقيقيًا لاستكشاف كيف تدور السبينات النووية وتفقد تماسكها في تجربة NMR، وكيف نفسّر الأطياف الناتجة. من المفيد أن نتراجع خطوة لنراجع لماذا كان هذا ممكنًا أصلًا، ولماذا هو أكثر من مجرد فضول.

الكيوبت ونواة سبين-1/2 هما، رياضيًا، الشيء نفسه: نظامان كميان ثنائيا المستوى يعيشان على كرة Bloch، تُدار بنبضات رنينية، وتدور حول محور، وتفقد التماسك لصالح محيطها. المفردات كلها في هذه الوحدة (تردد Larmor، الإطار الدوار، T1T_1 و T2T_2 و T2∗T_2^*، أهداب Ramsey، صدى Hahn) تنتمي إلى NMR والحوسبة الكمية على حد سواء. ما يسميه الكيميائي إزاحة كيميائية يسميه مهندس الكم دوران ZZ على كيوبت واحد؛ وما يسميه الكيميائي اقتران JJ يسميه مهندس الكم تفاعل ZZZZ. كان المجالان يصفان الفيزياء نفسها بكلمات مختلفة.

هذه الصلة تاريخية، لا مفاهيمية فقط: كان NMR من أوائل المنصات التي شغّلت خوارزميات كمية حقيقية. بحلول عام 2001، نفّذ جزيء في محلول خوارزمية Shor لتحليل العدد 15، مع عمل كل سبين نووي ككيوبت. كما منح NMR الحوسبة الكمية جزءًا كبيرًا من عدّة التحكم فيها. فصدى السبين، وسلاسل نبضات CPMG، والنبضات المركبة، والفصل الديناميكي، اخترعها كلها أخصائيو المطيافية قبل عقود من حاجة الكيوبتات إليها لمحاربة فقدان التماسك. وهناك تناظر مُرضٍ فيما فعلناه للتو: المجال الذي أنجب أوائل الحواسيب الكمية صار الآن شيئًا يستطيع الحاسوب الكمي محاكاته.

المراجعة والأسئلة​

المفاهيم الأساسية​

  • الكيوبت نظام ثنائي المستوى مُهندَس يكافئ سبين-1/2؛ و ∣0⟩/∣1⟩|0\rangle/|1\rangle والتردد والبوابات و T1T_1 و T2T_2 فيه نظائر مباشرة لكميات NMR.

  • تدير النبضة الرنينية متجه Bloch؛ وتحدد زاوية القلب الدوران، ويحدد منحنى Rabi/التذبذب P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) دورانَي π\pi و π/2\pi/2.

  • T1T_1 هو استرخاء الطاقة (السبين-الشبكة)؛ و T2T_2 هو استرخاء الطور (سبين-سبين)؛ ويشمل T2∗T_2^{*} إضافةً فقدان الطور الساكن العكوس الذي يعيد صدى Hahn تجميعه.

  • الجزيء مجموعة من السبينات المقترنة: تحدد الإزاحة الكيميائية موضع كل قمة، ويشطر اقتران JJ القمم إلى قمم متعددة وفق قاعدة n+1n+1، وهما معًا يشفّران البيئة الكيميائية والاتصال.

صح / خطأ​

  1. صح/خطأ تردد Larmor هو فجوة الطاقة بين حالتَي السبين مقسومة على ℏ\hbar.

  2. صح/خطأ نبضة 90∘90^\circ (π/2\pi/2) تقلب سبينًا في الاتزان قلبًا كاملًا.

  3. صح/خطأ T2∗T_2^{*} عمومًا أطول من T2T_2.

  4. صح/خطأ صدى Hahn يعيد تجميع فقدان الطور غير العكوس بين السبينات.

  5. صح/خطأ في نظام AX2_2 تظهر نواة A كثلاثية 1:2:1.

  6. صح/خطأ يتضاعف ثابت الاقتران JJ عندما تضاعف المجال المغناطيسي للمطياف.

  7. صح/خطأ إزالة Qiskit Pulse تجعل قياس T1T_1 على العتاد الحالي مستحيلًا.

إجابة قصيرة​

  1. اشرح في جملتين لماذا تُعد نبضة π\pi واحدة طريقة ضعيفة لاكتشاف خطأ في زاوية القلب مقداره 5%5\%، وكيف يحل تسلسل النبضات هذه المشكلة.

  2. تُظهر عينة خطوط NMR عريضة. من ناحية T2T_2، ماذا يخبرك ذلك، وما الكمية النظيرة في الكيوبت؟

  3. أعطِ النظير في الكيوبت لكل مما يلي: (أ) زمن التأخير بين التكرارات، (ب) نبضة 90∘90^\circ، (ج) الإزاحة الكيميائية.

  4. باستخدام قاعدة n+1n+1، اشرح لماذا تُظهر مجموعة الإيثيل (CH3_3CH2_2–) ثلاثية 1:2:1 ورباعية 1:3:3:1، وحدّد أي مجموعة تنتج أي قمة متعددة.

مسائل التحدي​

  1. سلسلة Carr-Purcell. عدّل صدى Hahn لإدراج 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. تلميح: ضع حاجزًا (barrier) بعد كل دوران؛ وإلا فقد يدمج المُحوِّل (transpiler) الدورانات المتكررة في بوابة واحدة مكافئة.

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

ملحق (اختياري): تطور بتقنية Trotter — دع فقدان التماسك يحدد عرض الخط​

في قسم مطيافية NMR لجزيء، يُطبَّق التطور e−iHte^{-iHt} في طبقة واحدة لا ينمو عمقها مع tt، لذلك لا يتناقص FID من تلقاء نفسه ونفرض عرض الخط بنافذة التنعيم e−t/T2e^{-t/T_2}. تتبع هذه الخلية الاختيارية النهج المعاكس. فهي تقسّم التطور إلى pp من خطوات Trotter وتضع حاجزًا (barrier) بعد كل خطوة (حتى لا يتمكن المُحوِّل من دمجها مجددًا في طبقة واحدة). تستخدم النقطة الزمنية kk عدد kk من الخطوات، فينمو عمق الدائرة مع الزمن.

لأن كل حد في HH يتبادل مع الآخر، فهذا دقيق: على محاكي بلا ضجيج يعطي القمم الحادة نفسها. لكن على Backend بنموذج ضجيج أو عتاد حقيقي يجعل تراكم خطأ البوابات 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()