Rezonanță magnetică nucleară cu Qiskit
Pentru acest modul Qiskit in Classrooms, studenții trebuie să aibă un mediu Python funcțional, cu următoarele pachete instalate:
qiskitv2.1 sau mai nouqiskit-ibm-runtimev0.40.1 sau mai nouqiskit-aerv0.17 sau mai nouscipyșimatplotlib(pentru ajustare și trasare)pylatexenc(pentru randarea diagramelor de circuit)
Pentru a configura și a instala pachetele de mai sus, consultă ghidul Install Qiskit. Pentru a rula joburi pe computere cuantice reale, studenții trebuie să creeze un cont IBM Quantum® urmând pașii din ghidul Set up your IBM Cloud® account.
Acest modul a fost testat și folosește aproximativ 5 minute de timp QPU. Aceasta este o estimare, iar utilizarea ta reală poate varia.
Introducere
Când un chimist rulează un experiment RMN, plasează o probă de spini nucleari într-un câmp magnetic puternic, aplică un impuls de radiofrecvență (RF) și măsoară semnalul care revine. Tehnica se bazează pe câteva idei din mecanica cuantică a unei particule cu spin 1/2 într-un câmp magnetic. Spinul are două niveluri de energie separate de o energie care corespunde unei frecvențe Larmor caracteristice. Un impuls rezonant la acea frecvență înclină spinul în planul transversal, unde acesta execută o mișcare de precesie și induce un semnal care apoi se estompează cu doi timpi caracteristici, și .
Un qubit supraconductor este o versiune concepută artificial a aceluiași obiect: un sistem cuantic controlabil cu două niveluri. Stările sale și sunt analogul spinului în sus și al spinului în jos, poarta pe care o aplicăm pentru a-l roti este analogul impulsului RF, iar el pierde coerența cu propriii și . Aceasta nu este doar o comparație vagă; unele dintre primele procesoare cuantice experimentale de la sfârșitul anilor 1990 și începutul anilor 2000 au folosit RMN.
În acest modul vei folosi un computer cuantic real pentru a rula aceleași experimente de caracterizare pe care le efectuează un spectroscopist RMN (sau un inginer cuantic care calibrează hardware-ul):
-
Rabi / nutație — acționarea spinului și observarea rotației sale. Așa calibrezi o rotație de sau .
-
— relaxarea spin-rețea, revenirea unui spin excitat la echilibru.
-
— relaxarea spin-spin (defazare), măsurată cu un experiment Ramsey și refocalizată cu un ecou Hahn.
-
Spectroscopie RMN (deplasare chimică și cuplaj ) — simularea unei molecule mici pe mai mulți qubiți și citirea spectrului ei de multipleți pentru a recupera conectivitatea prin regula .
Pentru fiecare concept oferim mai întâi teoria minimă, apoi implementăm experimentul în Qiskit urmând fluxul de lucru în patru pași Qiskit patterns.
Teorie
Particule cu spin 1/2 într-un câmp magnetic
Un nucleu cu spin 1/2 are un moment magnetic , unde este raportul giromagnetic. Într-un câmp static de-a lungul axei , cele două stări de spin se despart energetic (efectul Zeeman):
Diferența de energie definește frecvența Larmor (de rezonanță) . Momentul magnetic execută o precesie în jurul lui la această aceeași frecvență.
Pentru a roti spinul aplicăm un câmp RF oscilant la (sau aproape de) . În sistemul de referință care se rotește cu frecvența de acționare (sistemul rotitor), o acționare rezonantă rotește vectorul Bloch în jurul unei axe din planul -. Cât de mult se rotește (unghiul de basculare) este stabilit de intensitatea și durata impulsului. Două alegeri contează cel mai mult: un impuls de înclină un spin în planul transversal, iar un impuls de îl rotește complet la .
Traducerea între jargonul RMN și cel al calculului cuantic
Un qubit este un sistem cu două niveluri, identic matematic cu un spin 1/2 într-un câmp magnetic — așadar, chiar dacă rulăm pe un procesor cuantic și nu pe un nucleu, fizica este aceeași fizică ce stă la baza RMN. Doar vocabularul diferă. Acest tabel te ajută să vorbești atât cu chimiștii fizicieni, cât și cu specialiștii în calcul cuantic.
| RMN / spin 1/2 | Procesor cuantic (qubit) |
|---|---|
| Spin în sus / spin în jos | / |
| Diferența Zeeman | Energia de tranziție a qubitului |
| Frecvența Larmor | Frecvența qubitului |
| Impuls RF rezonant (unghi de basculare ) | Poartă de rotație pe un singur qubit |
| Impuls de / | / |
| Relaxare spin-rețea | Timpul de relaxare energetică a qubitului |
| Relaxare spin-spin | Timpul de defazare a qubitului |
| Nutație | Oscilații Rabi |
| Declin liber al inducției (FID), ecou Hahn | Secvențe Ramsey, secvențe de ecou |
Tot ce urmează este un exercițiu de măsurare a coloanei din dreapta și de citire a ei ca pe coloana din stânga.
Configurează mediul Qiskit
Urmăm fluxul de lucru Qiskit patterns pe tot parcursul:
-
Mapează problema pe un circuit cuantic.
-
Optimizează circuitul pentru execuție (transpilează pentru backend).
-
Execută folosind o primitivă IBM Quantum.
-
Post-procesează și analizează.
Mai întâi încărcăm serviciul runtime și alegem backend-ul real cel mai puțin ocupat. Dacă nu ți-ai salvat încă acreditările, consultă pagina de documentație despre cum să îți salvezi acreditările de autentificare.
# 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
Folosește celula de mai jos pentru a comuta între hardware real și un simulator local pentru întregul notebook. Rularea pe hardware este scopul modulului, dar dacă apar probleme de conectivitate sau de utilizare, simulatorul poate fi o variantă de rezervă. Setează USE_HARDWARE = True pentru a rula pe QPU-ul selectat mai sus.
import numpy as np
from itertools import combinations
from qiskit_aer import AerSimulator
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
USE_HARDWARE = True # <-- set True to run on the QPU selected above
USE_BATCH = False # Safer for a class with pauses; set True for an uninterrupted run
if USE_HARDWARE:
run_backend = backend # the real QPU selected above
else:
# Local testing mode: an Aer simulator that mimics the selected device -- its noise model,
# coupling map, and qubit count -- while layout choices still use real calibration data.
run_backend = AerSimulator.from_backend(backend)
# --- Choose connected qubits with low readout error. --------------------------------------
def readout_error(q):
props = backend.target["measure"].get((q,))
return props.error if props is not None and props.error is not None else 1.0
coupling_map = backend.coupling_map or backend.target.build_coupling_map()
edges = {tuple(sorted(edge)) for edge in coupling_map.get_edges() if edge[0] != edge[1]}
# AX needs one connected pair.
AX_LAYOUT = list(min(edges, key=lambda edge: sum(readout_error(q) for q in edge)))
# AX2 needs one qubit connected to two neighbours; the first entry is the central A qubit.
neighbors = {q: set() for q in range(backend.num_qubits)}
for q0, q1 in edges:
neighbors[q0].add(q1)
neighbors[q1].add(q0)
ax2_layouts = (
[a, x1, x2]
for a, adjacent in neighbors.items()
for x1, x2 in combinations(adjacent, 2)
)
AX2_LAYOUT = min(ax2_layouts, key=lambda layout: sum(readout_error(q) for q in layout))
# Use a long-lived qubit for the single-qubit experiments.
def best_qubit(backend):
return max(
range(backend.num_qubits), key=lambda q: backend.qubit_properties(q).t2 or 0.0
)
QUBIT = best_qubit(backend)
qp = backend.qubit_properties(QUBIT)
print(
f"Using qubit {QUBIT} for single-qubit experiments: "
f"T1 = {qp.t1 * 1e6:.0f} us, T2 = {qp.t2 * 1e6:.0f} us"
)
print(f"Using physical qubits {AX_LAYOUT} for AX")
print(f"Using physical qubits {AX2_LAYOUT} for AX2 (A first, then the two X spins)")
pm_q = generate_preset_pass_manager(
optimization_level=1, backend=run_backend, initial_layout=[QUBIT]
)
pm_ax = generate_preset_pass_manager(
optimization_level=1, backend=run_backend, initial_layout=AX_LAYOUT
)
pm_ax2 = generate_preset_pass_manager(
optimization_level=1, backend=run_backend, initial_layout=AX2_LAYOUT
)
# Job mode is the default because a classroom pause can deactivate a Batch. On the Open plan,
# a Batch has a 10-minute maximum lifetime and deactivates after 1 minute with no queued job.
batch = Batch(backend=run_backend) if USE_BATCH else None
execution_mode = batch if batch is not None else run_backend
sampler = Sampler(mode=execution_mode)
estimator = Estimator(mode=execution_mode)
estimator.options.resilience_level = 0 # raw expectation values, no error mitigation
estimator.options.default_shots = 1000
mode_name = "Batch" if USE_BATCH else "individual jobs"
device_name = "hardware" if USE_HARDWARE else "local simulator (runtime local mode)"
print(f"Ready. Running on {device_name} using {mode_name}.")
Experiment: oscilații Rabi (nutație)
Teorie
Acționează un spin la rezonanță și vectorul său Bloch se rotește constant de la un pol la celălalt — execută o nutație. Dacă aplicăm o rotație cu unghiul de basculare în jurul axei unui qubit care pornește din , probabilitatea de a-l găsi în este:
Aceasta este curba Rabi. Primul ei maxim este impulsul (inversiune completă, impulsul de ); un impuls este un sfert de tură (impulsul de care înclină un spin în planul transversal). În RMN, această curbă este modul în care calibrezi impulsurile.
Un experiment Rabi literal variază în timp amplitudinea sau durata acționării. Aceasta necesită control la nivel de impuls, care a fost eliminat în Qiskit v2.0. Aici variem în schimb unghiul de rotație al unei porți . Curba și identificarea / sunt identice; singura diferență este că alegem direct un unghi, în loc să calibrăm o durată de impuls. Pe hardware cu porți fracționare, pentru arbitrar rulează nativ.
O vedere pe sfera Bloch
Animația de mai jos arată nutația: o acționare rezonantă rotește constant vectorul Bloch în jurul axei , ducându-l de la spre și înapoi. Aceasta este chiar mișcarea pe care o măsoară curba Rabi.
Mapează problema pe un circuit
Construim un circuit parametrizat, urmat de o măsurătoare, și îl evaluăm ulterior la mai multe unghiuri simultan.
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")
Optimizare pentru execuție
Transpilează într-un circuit Instruction Set Architecture (ISA) pentru backend-ul ales.
rabi_isa = pm_q.run(rabi)
Execută
Variem de la la (două cicluri Rabi complete). Transmiterea unui array de valori ale parametrilor ca un singur Primitive Unified Bloc (PUB) le rulează pe toate într-un singur job.
angles = np.linspace(0, 4 * np.pi, 41)
job = sampler.run([(rabi_isa, angles.reshape(-1, 1))], shots=1000)
rabi_result = job.result()
Post-procesare și analiză
Extrage la fiecare unghi și compară cu .
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()

Verifică-ți înțelegerea
Pe curba pe care ai măsurat-o, atinge prima dată maximul la pulsul (). Presupune acum că un dispozitiv este calibrat greșit, astfel încât un unghi nominal de pe axa x produce de fapt o rotație de .
(a) La ce unghi nominal ar apărea acum primul maxim al lui ? (b) Ar fi un singur puls o metodă sensibilă de a detecta această eroare de 5%?
Răspuns
(a) Maximul apare acolo unde rotația reală este egală cu , adică la unghiul nominal — o mică deplasare a vârfului spre stânga.
(b) Nu. Un puls nominal produce o rotație reală de , ceea ce dă în loc de — o schimbare prea mică pentru a fi văzută în raport cu zgomotul de măsurare. De aceea, pentru a calibra precis, specialiștii în spectroscopie RMN amplifică eroarea aplicând rotația de mai multe ori (de exemplu metoda sau un tren lung de pulsuri), astfel încât o mică eroare per puls se acumulează într-o abatere mare și măsurabilă. Revenim la această idee în problemele provocatoare.
Experiment: (relaxare spin-rețea)
Teorie
Lăsat singur, un spin excitat cedează energie mediului înconjurător („rețeaua”) și revine spre echilibrul termic. Populația stării excitate scade exponențial cu timpul de relaxare spin-rețea :
Pentru un chimist, stabilește întârzierea de reciclare pe care trebuie s-o aștepți între scanări pentru RMN cantitativ și oferă informații despre rotirea moleculară și dinamică. Pe un qubit este timpul de relaxare a energiei și este unul dintre cele două numere principale folosite pentru a modela efectele de decoerență într-un computer cuantic.
Experimentul. Pregătește cu o poartă (un puls ), așteaptă o întârziere variabilă , apoi măsoară. Fracțiunea rămasă în trasează descreșterea.
O vizualizare pe sfera Bloch
Relaxarea readuce starea excitată la echilibru, astfel încât vectorul Bloch alunecă de la înapoi la de-a lungul axei .
Construiește și transpilează o scanare a întârzierii
Duratele întârzierilor trebuie să fie valori concrete (sunt programate în timpul hardware), așa că construim câte un circuit pentru fiecare întârziere.
# 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
Execută
t1_job = sampler.run([(c,) for c in t1_isa], shots=1000)
t1_res = t1_job.result()
Analiză — potrivirea exponențialei
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")
Measured T1 = 225.3 microseconds
Timpul exact variază de la un experiment la altul și de la un backend la altul, dar ar trebui să obții un rezultat de câteva sute de microsecunde.
Verifică-ți înțelegerea
În RMN cantitativ ți se spune să aștepți aproximativ între scanări. Folosind legea descreșterii, ce fracțiune din magnetizarea de echilibru s-a recuperat după ?
Răspuns
După , abaterea rămasă față de echilibru este , adică s-a recuperat aproximativ 99.3%.
Experiment: ,
Teorie
măsoară pierderea de energie. măsoară pierderea coerenței de fază în planul transversal. O putem observa cu un experiment Ramsey: un puls rotește toate spinurile până la ecuator, toate indicând în aceeași direcție. Ele execută precesie Larmor, dar fiecare precesează cu o frecvență ușor diferită, așa că se desfac în evantai de-a lungul ecuatorului, iar semnalul transversal net scade, cu un timp caracteristic de descreștere .
O paranteză pentru cititorii cuantici: Imaginea evantaiului arată o modalitate de a pierde coerența: o dispersie de faze definite care se anulează în medie. Merită să știi că nu este singurul mecanism. Coerența se poate pierde și pentru că faza se încâlcește (entanglement) cu mediul, astfel încât nu mai este deloc o valoare definită pe care o deține sistemul. Nu poți distinge cele două mecanisme doar urmărind descreșterea semnalului, deci imaginea evantaiului este o bună intuiție. Ai grijă să nu o citești ca pe povestea completă sau literală.
Două efecte defazează un spin:
- Decoerența ireversibilă (sau efectiv ireversibilă), provenită din cuplajul necontrolat cu alte grade de libertate, caracterizată de adevăratul . În RMN, aceasta provine în general din mișcarea moleculară care modulează cuplajele dintre spinuri. În calculul cuantic, corespunde cuplajelor nedorite dintre qubiți și mediu.
- Decalaje de frecvență statice sau lent variabile, reversibile, precum neomogenitatea câmpului, variația de frecvență de la un qubit la altul și erorile de calibrare. Combinate cu adevăratul , acestea produc timpul de descreștere aparent, mai scurt, , observat într-un experiment Ramsey.
Deci cum aflăm adevăratul dacă un experiment Ramsey ne spune doar despre ? Al doilea experiment, numit ecou Hahn, anulează tot zgomotul de al doilea tip pentru a găsi adevăratul . Începe la fel ca un experiment Ramsey, cu un puls , apoi spinurile se desfac în evantai ca înainte. După un timp , se aplică un puls . Pulsul inversează acumularea de fază cauzată de decalajele statice de frecvență, astfel încât spinurile se refocalizează după o a doua întârziere de aceeași durată, .
și sunt ambele numere importante de cunoscut, atât în chimie, cât și în calculul cuantic. În chimie, stabilește lățimea liniei pentru un experiment RMN standard (vei vedea ce înseamnă aceasta într-o secțiune ulterioară). ne poate spune multe despre molecule, precum rotirea moleculară, iar o schimbare a lui poate semnala că o moleculă s-a legat de ceva mai mare. În calculul cuantic, în general vrem ca atât , cât și să fie cât mai lungi, pentru a avea mai mult timp să efectuăm experimente înainte ca decoerența să copleșească sistemul. O întreagă familie de tehnici bazate pe ecoul Hahn prelungește acest timp anulând zgomotul reversibil, astfel încât circuitul să fie limitat în mod ideal de adevăratul , nu de . Aceasta se numește „decuplare dinamică”.
Implementare
Când implementăm experimentele Ramsey și ecou Hahn pe un qubit, citirea este ușor diferită față de cazul RMN. În RMN poți observa direct câmpurile oscilante induse de spinurile care execută precesie la frecvența Larmor în planul transversal. Dar cu un qubit putem măsura doar proiecția lui pe axa , adică dacă este sau . Prin urmare, pentru ambele experimente încheiem cu un puls suplimentar, urmat de o măsurătoare. Acest puls traduce informația de fază a qubitului într-o populație.
Mai jos rulăm fiecare experiment pentru a determina și .
Ramsey (): Două pulsuri cu timpul de întârziere
Primul puls înclină vectorul Bloch până la ecuator, iar următorul traduce faza vectorului Bloch într-o populație. Deoarece faza se schimbă în timp pe măsură ce vectorul Bloch execută precesie, vedem un model sinusoidal în populație în funcție de timpul de precesie . Pe măsură ce qubitul pierde coerența de fază, ar trebui să vedem amplitudinea acestei sinusoide scăzând.
Câteva subtilități în plus pentru cititorii cuantici: În experimentele de calcul cuantic, oscilațiile pe care le vedem nu sunt de fapt la frecvența Larmor, care este în domeniul GHz și mult prea rapidă pentru a fi rezolvată. Experimentele cuantice operează în sistemul de referință rotitor: un sistem care se rotește odată cu qubitul la (aproape) frecvența Larmor, astfel încât precesia rapidă este invizibilă, iar vectorul Bloch pare aproape nemișcat. Pentru a vedea franje, setăm în mod deliberat sistemul de referință rotitor ușor decalat față de frecvența Larmor. Vectorul Bloch pare atunci să execute precesie la acel mic decalaj, și obținem un semnal oscilant la frecvența decalajului. Mai jos nu fixăm decalajul la o singură valoare; îl alegem astfel încât aproximativ o duzină de franje să cuprindă fereastra de măsurare (pe care o dimensionăm după timpul de coerență al qubitului), ceea ce păstrează oscilația ușor de eșantionat fără aliasing. Încă o complicație: în Qiskit nu avem controlul la nivel de puls necesar pentru a schimba frecvența pulsului (care este cea care stabilește frecvența sistemului de referință rotitor). În schimb, adăugăm manual un decalaj efectiv, folosindu-l pentru a seta faza celui de-al doilea puls Ramsey. Este puțin artificial, dar rezultatul este același, iar descreșterea pe care o observăm este în continuare adevăratul .
O vizualizare pe sfera Bloch
Un puls înclină spinurile până la ecuator; fiecare execută apoi precesie cu o viteză ușor diferită și se desfac în evantai, astfel încât vectorul net (roșu) se micșorează pe măsură ce se defazează — această descreștere este .
# 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)
Ecou Hahn (): Adaugă un puls de refocalizare la mijloc
O vizualizare pe sfera Bloch
Ecoul Hahn: spinurile se desfac în evantai ca înainte, apoi un puls le răstoarnă, astfel încât precesia lor continuă le refocalizează din nou împreună (vectorul roșu crește la loc), recuperând coerența pierdută din cauza defazării statice.
# 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
Analiză: Potrivește și compară cele două descreșteri
# 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")

T2* (Ramsey) = 171.2 µs ; T2 (echo) = 326.0 µs
Aici vedem atât oscilația Ramsey, cât și descreșterea semnalului ecou Hahn, așa cum ne așteptam. Scalele de timp asociate fiecăreia dintre aceste descreșteri sunt și, respectiv, .
Verifică-ți înțelegerea
De obicei ar trebui să găsești . Explică de ce într-o singură propoziție și spune ce efect fizic elimină pulsul al ecoului Hahn, precum și ce nu poate elimina.
Răspuns
include atât defazarea ireversibilă, cât și dispersia statică reversibilă, așa că scade mai repede; pulsul al ecoului refocalizează defazarea statică, reversibilă (neomogenitatea câmpului / decalaje de frecvență fixe), lăsând doar defazarea spin-spin ireversibilă, care este adevăratul . Ecoul nu poate anula fluctuațiile care se schimbă între cele două jumătăți ale secvenței.
Spectroscopia RMN a unei molecule: deplasarea chimică și cuplajul
De la un spin la o moleculă
Experimentele de până acum au caracterizat un singur spin izolat. RMN-ul chimic real citește o moleculă (multe spinuri cuplate simultan) și folosește spectrul rezultat pentru a deduce trăsături ale structurii moleculei. Acesta ne poate spune despre două efecte:
-
Deplasarea chimică. Fiecare nucleu distinct chimic execută precesie cu o frecvență ușor diferită , deoarece electronii săi locali ecranează câmpul aplicat în măsuri diferite. Pozițiile vârfurilor identifică, prin urmare, mediile chimice (de exemplu un proton CH față de unul aromatic).
-
Cuplajul scalar . O interacțiune prin legături între două spinuri împarte fiecare vârf într-un multiplet. Un nucleu cu vecini echivalenți este împărțit în linii cu intensități binomiale, regula . Modelul de despicare dezvăluie conectivitatea (care nuclee sunt legate), iar distanța dintre linii este constanta de cuplaj .
Experimentul
Pentru a modela aceasta în computerul nostru cuantic, trebuie să implementăm un hamiltonian de spin pe qubiți. În limita cuplajului slab și a câmpului intens (diferențe de deplasare chimică cuplaje), părțile și ale cuplajului se anulează în medie, lăsând doar . Astfel, hamiltonianul este (în unități de frecvență unghiulară, ):
Fiecare nucleu este un qubit: deplasarea chimică este o rotație pe un singur qubit, iar cuplajul este o interacțiune pe doi qubiți.
Experimentul decurge astfel. Un puls înclină fiecare spin în planul transversal, iar spinurile execută precesie și se cuplează timp de . Înregistrăm magnetizarea transversală complexă însumată peste toate spinurile. Ca în experimentul Ramsey de mai sus, această magnetizare transversală trasează o sinusoidă descrescătoare în funcție de timpul de precesie . Când luăm transformata Fourier a semnalului, obținem un spectru cu vârfuri la deplasarea chimică , despicate de cuplajul scalar .
Simulăm evoluția moleculei transformând timpul în unghiuri de rotație: un mai mare înseamnă rotații și mai mari, nu un circuit mai lung sau mai adânc. Așadar, deși starea simulată avansează cu (asta face ca descreșterea liberă prin inducție (FID) să oscileze), qubiții fizici nu stau niciodată inactivi mai mult timp și nu există decoerență dependentă de . FID-ul simulat nu scade deci niciodată de la sine. Pentru un FID ideal infinit de lung, liniile spectrale ar fi infinit de înguste; fereastra noastră finită de eșantionare le dă în schimb o rezoluție finită și poate introduce scurgere spectrală. Liniile RMN reale au o lățime finită stabilită de . O adăugăm la loc înmulțind FID-ul cu o fereastră înainte de transformata Fourier. Aceasta se numește apodizare cu lărgire de linie, pe care spectroscopiștii o aplică în procesare pentru a stabili lățimea liniei (sacrificând rezoluția pentru raportul semnal-zgomot). (O celulă opțională din Anexa acestui modul arată o alternativă Trotterizată, în care descreșterea provine în schimb din decoerența reală, care crește cu adâncimea circuitului.)
Sistemul AX
Să începem cu așa-numitul sistem AX, o moleculă formată din două spinuri cuplate, distincte chimic. Etichetele A și X denotă cele două medii de spin distincte chimic. Literele sunt arbitrare, dar faptul că sunt departe în alfabet ilustrează deosebirea lor chimică.
Mapare și optimizare
Scriem o funcție care, date fiind decalajele de deplasare chimică și cuplajele , returnează un circuit parametrizat de pregătire a stării. Pulsul înclină spinurile până la ecuator, apoi rotațiile simulează evoluția stării sub termenii de deplasare chimică și de cuplaj . Fiecare este un mic decalaj de precesie în Hz față de o referință aleasă, sau purtătoarea sistemului de referință rotitor. Aici îl setăm manual; într-o moleculă reală, ecranarea electronică schimbă câmpul magnetic local și deplasează rezonanța fiecărui nucleu față de acea referință. Aceasta este modelată de porțile pe un singur qubit, câte una pentru fiecare spin, iar fiecare fixează unde se află vârful acelui spin în spectru. Poarta pe doi qubiți aplică cuplajul , care împarte fiecare vârf într-un multiplet și stabilește distanța dintre liniile sale.
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")
Execută și post-procesează
Evaluăm circuitul pe o grilă de timpi de evoluție, iar Estimator returnează și pentru fiecare. Le combinăm în FID-ul complex , aplicăm fereastra de apodizare și aplicăm transformata Fourier rezultatului pentru a obține spectrul.
# --- 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)}),
)

Expected AX lines (Hz): [-7.0, -5.0, 5.0, 7.0]
Extensie: un sistem AX și regula
Adăugăm acum un al doilea vecin echivalent. Într-un sistem AX, un spin A se cuplează, cu același , cu doi spini X echivalenți. (Reține că X este doar o etichetă pentru cele două spinuri echivalente; nu are legătură cu operatorii Pauli . Fiecare cuplaj de mai jos este tot .) Spinul A vede atunci patru configurații ale vecinilor ; cele două mixte coincid, deci A este despicat într-un triplet 1:2:1 ( linii). Fiecare spin X vede doar A și rămâne dublet. Spinurile echivalente nu se despică unele pe altele, deci nu includem niciun cuplaj X–X.
O moleculă reală cu acest model este 1,1,2-tricloroetanul (ClCH–CHCl). Când țintim spinurile hidrogenului (cea mai comună formă de RMN), singurul proton de hidrogen din CH joacă rolul lui A, iar cei doi protoni echivalenți CH sunt cei doi spini X. Acum numără pur și simplu vecinii: protonul CH are doi vecini (perechea CH), deci semnalul său se despică într-un triplet 1:2:1 (); fiecare proton CH are doar un vecin (CH-ul), deci semnalul CH este un dublet 1:1 (). Observă că grupul cu mai puțini protoni (CH) arată mai multe linii; numărul de linii reflectă câți vecini are un semnal, nu câți protoni îl împart.
Mai jos simulăm un sistem AX cu trei qubiți.
# 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)],
)
Verifică-ți înțelegerea
Citește fiecare întrebare, gândește-te la răspuns, apoi dă clic pe triunghi pentru a afișa soluția.
În spectrul AX, cele două dublete sunt separate (centru la centru) cu 12 Hz, iar cele două linii din fiecare dublet sunt la 2 Hz distanță. Care număr este diferența de deplasare chimică și care este constanta de cuplaj ? Dacă ai trece la un spectrometru cu un câmp magnetic de două ori mai mare, care dintre ele s-ar schimba?
Răspuns
Separarea de 12 Hz de la centru la centru este diferența de deplasare chimică (); distanța de 2 Hz dintre linii este constanta de cuplaj . Deplasările chimice provin din ecranarea lui , deci scalează cu câmpul. La un câmp dublu, separarea se dublează la 24 Hz. Cuplajul este o proprietate moleculară prin legături și nu depinde de câmp, deci rămâne 2 Hz. Chimiștii deosebesc deplasarea de cuplaj urmărind cum scalează o despicare cu câmpul.
Folosind regula , prezice multipletul A dacă s-ar adăuga un al treilea spin X echivalent (un sistem AX) și explică de ce cei doi spini X echivalenți din AX nu se despică unul pe altul.
Răspuns
Cu vecini echivalenți, A devine un cvartet 1:3:3:1. Spinurile echivalente nu se despică unele pe altele deoarece cuplajul lor deplasează nivelurile de energie relevante toate cu aceeași cantitate, astfel încât diferențele dintre aceste niveluri — frecvențele pe care le vedem — nu se schimbă.
Concluzie
Am folosit un computer cuantic real pentru a explora cum execută precesie și se decoerează spinurile nucleare într-un experiment RMN și cum să interpretăm spectrele rezultate. Merită să facem un pas înapoi pentru a revedea de ce a fost posibil acest lucru și de ce este mai mult decât o curiozitate.
Un qubit și un nucleu cu spin 1/2 sunt, matematic, același obiect: sisteme cuantice cu două niveluri aflate pe sfera Bloch, rotite de pulsuri rezonante, executând precesie în jurul unei axe și pierzând coerența în favoarea mediului. Întregul vocabular al acestui modul (frecvența Larmor, sistemul de referință rotitor, , și , franjele Ramsey, ecoul Hahn) aparține în egală măsură RMN-ului și calculului cuantic. Ceea ce un chimist numește deplasare chimică, un inginer cuantic numește rotație pe un singur qubit; ceea ce un chimist numește cuplaj , un inginer cuantic numește interacțiune . Cele două domenii au descris aceeași fizică în cuvinte diferite.
Această legătură este istorică, nu doar conceptuală: RMN-ul a fost una dintre primele platforme care au rulat algoritmi cuantici reali. Până în 2001, o moleculă în soluție executase algoritmul lui Shor pentru a factoriza 15, fiecare spin nuclear servind drept qubit. RMN-ul a oferit calculului cuantic și o mare parte din setul său de instrumente de control. Ecoul de spin, trenurile de pulsuri CPMG, pulsurile compuse și decuplarea dinamică au fost toate inventate de spectroscopiști cu zeci de ani înainte ca qubiții să aibă nevoie de ele pentru a combate decoerența. Există o simetrie satisfăcătoare în ceea ce tocmai am făcut: domeniul care a dat naștere primelor computere cuantice este acum ceva ce un computer cuantic poate simula.
Recapitulare și întrebări
Concepte esențiale
-
Un qubit este un sistem cu două niveluri creat artificial, echivalent cu un spin 1/2; , frecvența, porțile, și sunt analoagele directe ale mărimilor din RMN.
-
Un puls rezonant rotește vectorul Bloch; unghiul de basculare stabilește rotația, iar curba Rabi/de nutație identifică rotațiile și .
-
este relaxarea energiei (spin-rețea); este relaxarea fazei (spin-spin); include în plus defazarea statică reversibilă pe care un ecou Hahn o refocalizează.
-
O moleculă este un set de spinuri cuplate: deplasarea chimică stabilește poziția fiecărui vârf, iar cuplajul împarte vârfurile în multipleți după regula , care împreună codifică mediul chimic și conectivitatea.
Adevărat / fals
-
A/F Frecvența Larmor este diferența de energie dintre cele două stări de spin împărțită la .
-
A/F Un puls de () inversează complet un spin aflat la echilibru.
-
A/F este în general mai lung decât .
-
A/F Ecoul Hahn refocalizează defazarea spin-spin ireversibilă.
-
A/F Într-un sistem AX, nucleul A apare ca un triplet 1:2:1.
-
A/F Constanta de cuplaj se dublează când dublezi câmpul magnetic al spectrometrului.
-
A/F Eliminarea Qiskit Pulse face imposibilă măsurarea lui pe hardware-ul actual.
Răspuns scurt
-
Explică în două propoziții de ce un singur puls este o metodă slabă de a detecta o eroare de a unghiului de basculare și cum remediază aceasta înlănțuirea pulsurilor.
-
O probă arată linii RMN late. În termeni de , ce îți spune aceasta și care este mărimea analogă la un qubit?
-
Dă analogul la qubit pentru fiecare: (a) întârzierea de reciclare, (b) pulsul de , (c) deplasarea chimică.
-
Folosind regula , explică de ce o grupare etil (CHCH–) arată un triplet 1:2:1 și un cvartet 1:3:3:1 și identifică ce grup produce ce multiplet.
Probleme provocatoare
-
Tren Carr-Purcell. Modifică ecoul Hahn pentru a insera pulsuri echidistante (un tren în stil CPMG) și măsoară în funcție de . Discută de ce mai multe pulsuri de refocalizare pot prelungi aparent.
-
Amplificarea erorii de puls. Construiește un circuit care aplică de ori asupra lui și trasează în funcție de pentru un unghi setat greșit în mod deliberat, . Arată cum eroarea devine vizibilă pe măsură ce crește. Indiciu: pune o barieră după fiecare rotație; altfel transpiler-ul poate combina rotațiile repetate într-o singură poartă echivalentă.
-
Gruparea etil. Folosind
fid_circuit, construiește spectrul de protoni al unui fragment etil (CH–CH): cinci qubiți, cu cei trei protoni CH având o singură deplasare chimică și cei doi protoni CH alta, și fiecare pereche CH–CH cuplată cu același (fără cuplaj în interiorul unui grup echivalent). Prezice modelul cu regula , apoi simulează-l; ar trebui să regăsești un cvartet 1:3:3:1 (CH) și un triplet 1:2:1 (CH).
Anexă (opțional): evoluție Trotterizată — lasă decoerența să stabilească lățimea liniei
În secțiunea Spectroscopia RMN a unei molecule, evoluția este aplicată într-un singur strat a cărui adâncime nu crește cu , deci FID-ul nu scade de la sine și impunem lățimea liniei cu fereastra de apodizare . Această celulă opțională adoptă abordarea opusă. Ea împarte evoluția în pași Trotter și pune o barieră după fiecare pas (astfel încât transpiler-ul să nu-i poată îmbina înapoi într-un singur strat). Punctul de timp folosește pași, deci adâncimea circuitului crește cu timpul.
Deoarece toți termenii din comută, aceasta este exactă: pe un simulator fără zgomot dă aceleași vârfuri înguste. Dar pe un backend cu model de zgomot sau pe hardware real, eroarea acumulată a porților face FID-ul să scadă de la sine, iar această descreștere este cea care stabilește lățimea liniei. Aceasta este ideea lățimea liniei din secțiunea experimentul , , apărând acum din decoerență reală în loc de o fereastră de procesare. Rulăm în acest mod atât sistemul AX (două dublete), cât și sistemul AX (un triplet 1:2:1 plus un dublet).
Două note practice de luat în considerare. În primul rând, costul este adâncimea: cel mai adânc circuit AX folosește de porți pe doi qubiți, iar versiunea AX aproximativ dublu, deci ambele sunt mult mai zgomotoase și mai costisitoare decât versiunea exactă. În al doilea rând, fiecare sistem de spin este tot trimis ca un singur job, iar fiecare punct de timp reutilizează aceeași dispunere a qubiților fizici selectată în secțiunea de spectroscopie. Dacă USE_BATCH=True, aceste joburi folosesc și Batch-ul opțional valabil pentru tot notebook-ul.
# 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()