주 콘텐츠로 건너뛰기
이 프로젝트는 베타 단계입니다. 버그를 발견하셨나요? GitHub 이슈 만들기. 질문이나 아이디어가 있으신가요? Discussions에서 질문하기 — 피드백을 기다립니다!
이 페이지의 번역 품질은 어떻습니까?

Qiskit으로 살펴보는 핵자기공명

이 Qiskit in Classrooms 모듈을 사용하려면 학생이 다음 패키지가 설치된 작동하는 Python 환경을 갖추고 있어야 합니다.

  • qiskit v2.1 or newer
  • qiskit-ibm-runtime v0.40.1 or newer
  • qiskit-aer v0.17 or newer
  • scipy and matplotlib (for fitting and plotting)
  • pylatexenc (for rendering circuit diagrams)

위 패키지를 설정하고 설치하려면 Qiskit 설치 가이드를 참조하세요. 실제 양자 컴퓨터에서 작업을 실행하려면 학생은 IBM Cloud® 계정 설정 가이드의 단계에 따라 IBM Quantum® 계정을 설정해야 합니다.

이 모듈은 테스트를 거쳤으며 QPU 시간을 약 5분 사용합니다. 이는 추정치이며 실제 사용량은 다를 수 있습니다.

소개​

화학자가 NMR 실험을 수행할 때는 핵스핀 시료를 강한 자기장 안에 놓고, 라디오 주파수(RF) 펄스를 가한 뒤, 되돌아오는 신호를 측정합니다. 이 기법은 자기장 속 스핀-1/2 입자의 양자역학에 나오는 몇 가지 개념에 기반합니다. 스핀에는 특성 라모어 주파수에 해당하는 에너지만큼 떨어진 두 개의 에너지 준위가 있습니다. 그 주파수의 공명 펄스는 스핀을 가로 평면으로 기울이고, 스핀은 그곳에서 세차 운동을 하며 신호를 유도합니다. 이 신호는 이후 T1T_1과 T2T_2라는 두 가지 특성 시간에 따라 사라집니다.

초전도 큐비트는 같은 대상을 공학적으로 구현한 것으로, 제어할 수 있는 두 준위 양자 시스템입니다. ∣0⟩|0\rangle과 ∣1⟩|1\rangle 상태는 스핀 업과 스핀 다운에 대응하고, 큐비트를 회전시키기 위해 적용하는 게이트는 RF 펄스에 대응하며, 큐비트도 자체적인 T1T_1과 T2T_2로 결맞음을 잃습니다. 이는 단순한 느슨한 비유가 아닙니다. 1990년대 후반과 2000년대 초반의 가장 초기 실험용 양자 프로세서 중 일부는 NMR을 사용했습니다.

이 모듈에서는 실제 양자 컴퓨터를 사용하여 NMR 분광학자(또는 하드웨어를 보정하는 양자 엔지니어)가 수행하는 것과 동일한 특성화 실험을 실행합니다.

  • 라비 / 뉴테이션 — 스핀을 구동하고 회전하는 모습을 관찰합니다. 이것이 π\pi 또는 π/2\pi/2 회전을 보정하는 방법입니다.

  • T1T_1 — 스핀-격자 이완, 즉 들뜬 스핀이 평형 상태로 되돌아가는 감쇠입니다.

  • T2T_2 — 스핀-스핀 이완(위상 소실)으로, 램지 실험으로 측정하고 한 에코로 재초점화합니다.

  • NMR 분광학(화학적 이동과 JJ-결합) — 여러 큐비트로 작은 분자를 시뮬레이션하고 다중선 스펙트럼을 읽어 n+1n+1 규칙으로 연결 관계를 복원합니다.

각 개념마다 먼저 최소한의 이론을 설명한 다음, 네 단계로 이루어진 Qiskit 패턴 워크플로에 따라 Qiskit으로 실험을 구현합니다.

이론​

자기장 속 스핀-1/2 입자​

스핀-1/2 핵은 자기 모멘트 μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}를 가지며, 여기서 γ\gamma는 자기회전비입니다. zz 방향의 정적 자기장 B0B_0 안에서는 두 스핀 상태의 에너지가 갈라집니다(제이만 효과).

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 또는 그 근처에서 진동하는 RF 장을 가합니다. 구동 주파수로 회전하는 좌표계(회전 좌표계)에서 공명 구동은 블로흐 벡터를 xx-yy 평면의 한 축을 중심으로 회전시킵니다. 얼마나 회전하는지(플립 각)는 펄스의 세기와 지속 시간으로 정해집니다. 가장 중요한 두 가지 경우가 있습니다. 90∘90^\circ 펄스는 +1/2+1/2 스핀을 가로 평면으로 기울이고, 180∘180^\circ 펄스는 −1/2-1/2로 완전히 회전시킵니다.

NMR과 양자 컴퓨팅 용어 사이의 번역​

큐비트는 자기장 속 스핀-1/2과 수학적으로 동일한 두 준위 시스템입니다 — 따라서 핵이 아니라 양자 프로세서에서 실행하더라도, NMR의 바탕이 되는 물리와 같은 물리입니다. 어휘만 다를 뿐입니다. 이 표는 물리화학자와 양자 컴퓨팅 전문가 모두와 대화하는 데 도움이 됩니다.

NMR / 스핀-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^\circ 펄스RX(π/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. 후처리하고 분석합니다.

먼저 런타임 서비스를 불러오고 가장 한가한 실제 백엔드를 선택합니다. 아직 자격 증명을 저장하지 않았다면 로그인 자격 증명 저장 방법을 설명한 문서 페이지를 참조하세요.

# 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

아래 셀을 사용하여 노트북 전체에서 실제 하드웨어와 로컬 시뮬레이터 사이를 전환하세요. 하드웨어에서 실행하는 것이 이 모듈의 핵심이지만, 연결성이나 사용량 문제가 있으면 시뮬레이터를 대안으로 쓸 수 있습니다. 위에서 선택한 QPU에서 실행하려면 USE_HARDWARE = True로 설정하세요.

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

실험: 라비 진동(뉴테이션)​

이론​

스핀을 공명 상태로 구동하면 블로흐 벡터가 극에서 극으로 꾸준히 회전합니다 — 즉 뉴테이션이 일어납니다. ∣0⟩|0\rangle에서 시작하는 큐비트에 xx축을 중심으로 플립 각 θ\theta만큼 회전을 적용하면, ∣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 펄스는 4분의 1 회전(스핀을 가로 평면으로 기울이는 90∘90^\circ 펄스)입니다. NMR에서는 이 곡선이 펄스를 보정하는 방법입니다.

실험상의 주의 사항

문자 그대로의 라비 실험은 구동 진폭이나 지속 시간을 시간에 따라 스윕합니다. 이를 위해서는 Qiskit v2.0에서 제거된 펄스 수준 제어가 필요합니다. 여기서는 대신 RX(θ)R_X(\theta) 게이트의 회전 각 θ\theta를 스윕합니다. 곡선과 π\pi/π/2\pi/2의 대응은 동일하며, 차이점은 펄스 길이를 보정하는 대신 각도를 직접 선택한다는 것뿐입니다. 분수 게이트를 지원하는 하드웨어에서는 임의의 θ\theta에 대한 RX(θ)R_X(\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(라비 주기 두 번 전체)까지 스윕합니다. 매개변수 값 배열을 하나의 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)에서 처음으로 최댓값에 도달합니다. 이제 장치가 잘못 보정되어 x축의 공칭 각도 θ\theta가 실제로는 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이 아닙니다 — 측정 노이즈 속에서 알아보기에는 너무 작은 변화입니다. 그래서 정밀하게 보정하기 위해 NMR 분광학자들은 회전을 여러 번 적용하여(예: 360∘360^\circ 방법이나 긴 펄스 열) 오류를 증폭합니다. 이렇게 하면 펄스당 작은 오류가 누적되어 측정 가능한 큰 편차가 됩니다. 이 아이디어는 도전 문제에서 다시 다룹니다.

실험: T1T_1(스핀-격자 이완)​

이론​

그대로 두면 들뜬 스핀은 주변("격자")에 에너지를 내주고 열평형 쪽으로 이완합니다. 들뜬 상태의 개체수는 스핀-격자 이완 시간 T1T_1에 따라 지수적으로 감쇠합니다.

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

화학자에게 T1T_1은 정량적 NMR에서 스캔 사이에 기다려야 하는 재순환 지연을 결정하며, 분자 텀블링과 동역학에 관한 정보를 알려 줍니다. 큐비트에서는 에너지 이완 시간이며, 양자 컴퓨터의 결어긋남 효과를 모델링하는 데 쓰이는 두 가지 주요 수치 중 하나입니다.

실험. XX 게이트(π\pi 펄스)로 ∣1⟩|1\rangle을 준비하고, 가변 지연 τ\tau만큼 기다린 뒤 측정합니다. 여전히 ∣1⟩|1\rangle에 남아 있는 비율이 감쇠 곡선을 그립니다.

블로흐 구 관점​

T1T_1 이완은 들뜬 상태를 평형으로 되돌리므로, 블로흐 벡터는 zz축을 따라 ∣1⟩|1\rangle에서 ∣0⟩|0\rangle으로 미끄러져 올라갑니다.

지연 스캔 구성 및 트랜스파일​

지연 시간은 구체적인 값이어야 하므로(하드웨어 시간 단위로 스케줄링되기 때문입니다) 지연마다 하나의 회로를 만듭니다.

# 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 시간은 실험마다, 백엔드마다 다르지만, 대략 수백 마이크로초 정도의 결과를 볼 수 있을 것입니다.

이해도 확인​

정량 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는 횡평면에서의 위상 결맞음 손실을 측정합니다. 이는 램지 실험으로 관찰할 수 있습니다. π/2\pi/2 펄스가 모든 스핀을 적도 방향으로 회전시켜 모두 같은 방향을 가리키게 합니다. 스핀들은 라모어 세차 운동을 하지만 각각 조금씩 다른 주파수로 세차 운동을 하므로 적도를 따라 부채꼴로 퍼지고, 순 횡방향 신호는 T2∗T_2^*라는 특성 감쇠 시간으로 감쇠합니다.

양자 분야 독자를 위한 참고: 부채꼴로 퍼지는 그림은 결맞음을 잃는 한 가지 방식, 즉 서로 평균되어 사라지는 확정된 위상들의 분포를 보여줍니다. 이것이 유일한 메커니즘은 아니라는 점을 알아 두면 좋습니다. 위상이 환경과 얽히게 되어 시스템이 더 이상 확정된 값을 갖지 않기 때문에 결맞음이 사라질 수도 있습니다. 신호 감쇠를 관찰하는 것만으로는 두 메커니즘을 구별할 수 없으므로, 부채꼴 그림은 좋은 직관이 됩니다. 다만 이를 완전하거나 문자 그대로의 설명으로 받아들이지 않도록 유의하세요.

두 가지 효과가 스핀의 위상을 흩뜨립니다.

  • 다른 자유도와의 통제되지 않은 결합에서 비롯되는 비가역적(또는 사실상 비가역적) 결어긋남으로, 진짜 T2T_2로 특징지어집니다. NMR에서는 일반적으로 분자 운동이 스핀 간 결합을 변조하는 데서 나옵니다. 양자 컴퓨팅에서는 큐비트와 환경 사이의 원치 않는 결합에 해당합니다.
  • 자기장 불균일성, 큐비트 간 주파수 편차, 보정 오류 같은 정적이거나 천천히 변하는 가역적 주파수 오프셋. 진짜 T2T_2와 결합되어 램지 실험에서 관찰되는 더 짧은 겉보기 감쇠 시간 T2∗T_2^*를 만들어 냅니다.

그렇다면 램지 실험이 T2∗T_2^*만 알려 준다면 진짜 T2T_2는 어떻게 구할까요? **한 에코(Hahn echo)**라고 하는 두 번째 실험은 두 번째 유형의 잡음을 모두 상쇄하여 진짜 T2T_2를 찾아냅니다. 이 실험은 램지 실험과 같이 π/2\pi/2 펄스로 시작하고, 스핀들은 앞에서처럼 부채꼴로 퍼집니다. 시간 τ\tau가 지난 후 π\pi 펄스를 가합니다. 이 펄스는 정적 주파수 오프셋으로 인해 누적된 위상을 뒤집으므로, 같은 길이 τ\tau의 두 번째 지연 후에 스핀들이 다시 모입니다.

T2∗T_2^*와 T2T_2는 화학 분야와 양자 컴퓨팅 분야 모두에서 알아야 할 중요한 값입니다. 화학 분야에서 T2∗T_2^*는 표준 NMR 실험의 선폭을 결정합니다(이것이 무슨 뜻인지는 이후 섹션에서 보게 됩니다). T2T_2는 분자의 회전 운동 같은 분자에 관한 많은 정보를 알려 주며, T2T_2의 변화는 분자가 더 큰 무언가에 결합했음을 나타낼 수 있습니다. 양자 컴퓨팅 분야에서는 일반적으로 T2T_2와 T2∗T_2^*가 모두 가능한 한 길기를 원하는데, 결어긋남이 시스템을 압도하기 전에 실험을 수행할 시간이 더 많아지기 때문입니다. 한 에코에 기반한 여러 기법은 가역적 잡음을 상쇄하여 이 시간을 늘리며, 회로가 이상적으로는 T2∗T_2^*가 아닌 진짜 T2T_2에 의해서만 제한되도록 합니다. 이를 "동적 디커플링"이라고 합니다.

구현​

큐비트에서 램지 및 한 에코 실험을 구현할 때는 NMR의 경우와 판독 방식이 약간 다릅니다. NMR에서는 횡평면에서 라모어 주파수로 세차 운동하는 스핀이 유도하는 진동 자기장을 직접 관찰할 수 있습니다. 하지만 큐비트에서는 ZZ축 방향의 투영, 즉 ∣0⟩\vert 0 \rangle인지 ∣1⟩\vert 1 \rangle인지만 측정할 수 있습니다. 따라서 두 실험 모두 마지막에 추가 π/2\pi/2 펄스를 가한 뒤 측정합니다. 이 π/2\pi/2 펄스는 큐비트의 위상 정보를 포퓰레이션으로 변환합니다.

아래에서 각 실험을 실행하여 T2∗T_2^*와 T2T_2를 구합니다.

램지(T2∗T_2^{*}): 지연 시간 τ\tau를 둔 두 개의 π/2\pi/2 펄스​

첫 번째 π/2\pi/2 펄스는 블로흐 벡터를 적도로 기울이고, 두 번째 펄스는 블로흐 벡터의 위상을 포퓰레이션으로 변환합니다. 블로흐 벡터가 세차 운동하면서 위상이 시간에 따라 변하므로, 세차 시간 τ\tau의 함수로 포퓰레이션에서 사인 곡선 패턴이 나타납니다. 큐비트가 위상 결맞음을 잃으면 이 사인 곡선의 진폭이 감쇠하는 것을 볼 수 있습니다.

양자 분야 독자를 위한 좀 더 세부적인 설명: 양자 컴퓨팅 실험에서 우리가 보는 진동은 실제로는 라모어 주파수에서의 진동이 아닙니다. 라모어 주파수는 GHz 대역으로 너무 빨라서 분해할 수 없기 때문입니다. 양자 실험은 회전 좌표계에서 이루어집니다. 이는 큐비트와 함께 (거의) 라모어 주파수로 회전하는 좌표계로, 빠른 세차 운동은 보이지 않고 블로흐 벡터는 거의 정지해 있는 것처럼 보입니다. 무늬를 보려면 회전 좌표계를 라모어 주파수에서 일부러 약간 어긋나게 설정합니다. 그러면 블로흐 벡터는 그 작은 이조(detuning)로 세차 운동하는 것처럼 보이고, 이조 주파수에서 진동하는 신호를 얻습니다. 아래에서는 이조를 한 값으로 고정하지 않고, 측정 구간(큐비트의 결맞음 시간에 맞춰 크기를 정합니다)에 약 12개의 무늬가 들어가도록 선택하며, 이렇게 하면 에일리어싱 없이 진동을 쉽게 샘플링할 수 있습니다. 한 가지 더 번거로운 점이 있습니다. 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입니다. 에코는 시퀀스의 전반부와 후반부 사이에서 변하는 요동은 되돌릴 수 없습니다.

분자의 NMR 분광법: 화학적 이동과 JJ 결합​

스핀 하나에서 분자로​

지금까지의 실험은 고립된 단일 스핀의 특성을 다루었습니다. 실제 화학 NMR은 분자(결합된 여러 스핀이 동시에 있는 계)를 읽고, 그 결과로 얻은 스펙트럼을 사용하여 분자 구조의 특징을 추론합니다. 이를 통해 두 가지 효과를 알 수 있습니다.

  • 화학적 이동. 화학적으로 구별되는 각 핵은 국소 전자가 가해진 자기장을 서로 다른 정도로 차폐하기 때문에 조금씩 다른 주파수 ν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라면 스펙트럼 선이 무한히 날카롭겠지만, 유한한 샘플링 구간은 선에 유한한 분해능을 부여하고 스펙트럼 누설을 일으킬 수 있습니다. 실제 NMR 선은 T2∗T_2^*로 정해지는 유한한 폭을 가집니다. 푸리에 변환 전에 FID에 윈도우 e−t/T2∗e^{-t/T_2^{*}}를 곱하여 이를 다시 더해 줍니다. 이를 **선폭 확장 아포다이제이션(line-broadening apodization)**이라고 하며, 분광학자들이 처리 과정에서 선폭을 정하기 위해 적용합니다(분해능과 신호 대 잡음비를 맞바꿉니다). (이 모듈 부록의 선택 사항 셀에서는 트로터화 대안을 보여 주는데, 여기서는 회로 깊이에 따라 커지는 실제 결어긋남에서 감쇠가 발생합니다.)

AX 계​

이른바 AX 계부터 시작해 보겠습니다. 이는 결합된, 화학적으로 구별되는 두 스핀으로 이루어진 분자입니다. 레이블 A와 X는 화학적으로 구별되는 두 스핀 환경을 나타냅니다. 문자 자체는 임의적이지만, 알파벳에서 서로 멀리 떨어져 있다는 점이 두 스핀의 화학적 차이를 보여 줍니다.

매핑 및 최적화​

화학적 이동 오프셋 νi\nu_i와 결합 JijJ_{ij}가 주어지면 매개변수화된 상태 준비 회로를 반환하는 함수를 하나 작성합니다. π/2\pi/2 펄스가 스핀을 적도로 기울인 다음, ZZ 회전이 화학적 이동 항과 JJ 결합 항에 따른 상태의 시간 발전을 시뮬레이션합니다. 각 νi\nu_i는 선택한 기준 또는 회전 좌표계 반송파에 대한 Hz 단위의 작은 세차 운동 오프셋입니다. 여기서는 이를 직접 설정하지만, 실제 분자에서는 전자 차폐가 국소 자기장을 바꾸어 각 핵의 공명을 그 기준에 대해 이동시킵니다. 이는 스핀마다 하나씩 있는 단일 큐비트 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로 결합하고, 아포다이제이션 윈도우를 적용한 다음 푸리에 변환하여 스펙트럼을 얻습니다.

# --- 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)입니다. 수소 스핀을 대상으로 할 때(가장 일반적인 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 규칙을 사용하여, 세 번째 등가 X 스핀이 추가된다면(AX3_3 계) A 다중선이 어떻게 될지 예측하고, AX2_2에서 두 등가 X 스핀이 서로를 갈라지게 하지 않는 이유를 설명해 보세요.

정답

n=3n=3개의 등가 이웃이 있으므로 A는 1:3:3:1 사중선이 됩니다. 등가 스핀은 서로를 갈라지게 하지 않는데, 이들의 결합은 관련된 에너지 준위를 모두 같은 양만큼 이동시키므로, 그 준위 사이의 간격 — 즉 우리가 보는 주파수 — 은 변하지 않기 때문입니다.

결론​

우리는 실제 양자 컴퓨터를 사용하여 NMR 실험에서 핵 스핀이 어떻게 세차 운동하고 결어긋나는지, 그리고 그 결과 스펙트럼을 어떻게 해석하는지 살펴보았습니다. 이것이 애초에 어떻게 가능했는지, 그리고 왜 단순한 호기심 이상인지 한 걸음 물러나 짚어 볼 만합니다.

큐비트와 스핀-1/2 핵은 수학적으로 같은 대상입니다. 둘 다 블로흐 구 위에 있는 두 준위 양자 시스템이며, 공명 펄스로 회전되고, 한 축을 중심으로 세차 운동하며, 주변 환경으로 인해 결맞음을 잃습니다. 이 모듈의 모든 용어(라모어 주파수, 회전 좌표계, T1T_1, T2T_2, T2∗T_2^*, 램지 무늬, 한 에코)는 NMR과 양자 컴퓨팅 모두에 똑같이 해당합니다. 화학자가 화학적 이동이라고 부르는 것을 양자 엔지니어는 단일 큐비트 ZZ 회전이라고 부르고, 화학자가 JJ 결합이라고 부르는 것을 양자 엔지니어는 ZZZZ 상호작용이라고 부릅니다. 두 분야는 같은 물리를 다른 말로 설명해 왔습니다.

이 연결은 개념적일 뿐만 아니라 역사적이기도 합니다. NMR은 실제 양자 알고리즘을 실행한 최초의 플랫폼 중 하나였습니다. 2001년까지 용액 속의 한 분자가 각 핵 스핀을 큐비트로 사용하여 쇼어 알고리즘으로 15를 인수분해했습니다. NMR은 또한 양자 컴퓨팅에 제어 도구의 상당 부분을 물려주었습니다. 스핀 에코, CPMG 펄스열, 복합 펄스, 동적 디커플링은 모두 큐비트가 결어긋남과 싸우기 위해 이를 필요로 하기 수십 년 전에 분광학자들이 발명한 것입니다. 우리가 방금 한 일에는 만족스러운 대칭성이 있습니다. 초기 양자 컴퓨터를 탄생시킨 분야를 이제는 양자 컴퓨터가 시뮬레이션할 수 있게 된 것입니다.

복습 및 문제​

핵심 개념​

  • 큐비트는 스핀-1/2과 동등한 공학적으로 구현한 두 준위 시스템입니다. ∣0⟩/∣1⟩|0\rangle/|1\rangle, 주파수, 게이트, T1T_1, T2T_2는 NMR 물리량의 직접적인 대응물입니다.

  • 공명 펄스는 블로흐 벡터를 회전시킵니다. 뒤집힘 각도가 회전을 정하며, 라비/章동 곡선 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. 어떤 시료가 넓은 NMR 선을 보입니다. 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 방식 펄스열)를 삽입하고 nn의 함수로 T2T_2를 측정하세요. 재집속 펄스를 더 많이 쓰면 겉보기 T2T_2가 길어질 수 있는 이유를 논의하세요.

  2. 펄스 오류 증폭. ∣0⟩|0\rangle에 RX(π)R_X(\pi)를  n\,n번 적용하는 회로를 만들고, 의도적으로 잘못 설정한 각도 RX(1.05π)R_X(1.05\pi)에 대해 P1P_1을 nn에 대해 그려 보세요. 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)을 얻을 수 있을 것입니다.

부록(선택 사항): 트로터화된 시간 발전 — 결어긋남이 선폭을 정하게 하기​

분자의 NMR 분광법 섹션에서는 시간 발전 e−iHte^{-iHt}를 tt에 따라 깊이가 커지지 않는 단일 층으로 적용하므로 FID가 스스로 감쇠하지 않으며, 선폭은 e−t/T2e^{-t/T_2} 아포다이제이션 윈도우로 부여했습니다. 이 선택 사항 셀은 반대 접근법을 취합니다. 시간 발전을 pp개의 트로터 단계로 나누고 각 단계 뒤에 배리어를 둡니다(트랜스파일러가 이를 다시 하나의 층으로 합칠 수 없도록). 시점 kk는 kk개의 단계를 사용하므로 회로 깊이가 시간에 따라 커집니다.

HH의 모든 항이 교환하므로 이는 정확합니다. 잡음 없는 시뮬레이터에서는 똑같이 날카로운 피크를 얻습니다. 하지만 노이즈 모델 백엔드나 실제 하드웨어에서는 누적되는 게이트 오류로 FID가 스스로 감쇠하며, 이 감쇠가 선폭을 정합니다. 이는 T2T_2, T2∗T_2^* 실험 섹션의 T2↔T_2\leftrightarrow선폭 개념이 처리 윈도우 대신 실제 결어긋남에서 나타나는 것입니다. 이 방식으로 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()