Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

Risonanza magnetica nucleare con Qiskit

Per questo modulo di Qiskit in Classrooms, gli studenti devono disporre di un ambiente Python funzionante con i seguenti pacchetti installati:

  • qiskit v2.1 o successiva
  • qiskit-ibm-runtime v0.40.1 o successiva
  • qiskit-aer v0.17 o successiva
  • scipy e matplotlib (per il fitting e la creazione di grafici)
  • pylatexenc (per il rendering dei diagrammi dei circuiti)

Per configurare e installare i pacchetti indicati sopra, consulta la guida Installare Qiskit. Per eseguire job su computer quantistici reali, gli studenti devono creare un account con IBM Quantum® seguendo i passaggi della guida Configurare il tuo account IBM Cloud®.

Questo modulo è stato testato e utilizza circa 5 minuti di tempo QPU. Si tratta di una stima e il tuo utilizzo effettivo potrebbe variare.

Introduzione​

Quando un chimico esegue un esperimento NMR, colloca un campione di spin nucleari in un forte campo magnetico, applica un impulso a radiofrequenza (RF) e misura il segnale che ritorna. La tecnica si basa su alcune idee della meccanica quantistica di una particella di spin 1/2 in un campo magnetico. Lo spin ha due livelli di energia separati da un'energia che corrisponde a una caratteristica frequenza di Larmor. Un impulso risonante a quella frequenza porta lo spin nel piano trasversale, dove precede e induce un segnale che poi si attenua con due tempi caratteristici, T1T_1 e T2T_2.

Un qubit superconduttore è una versione ingegnerizzata dello stesso oggetto: un sistema quantistico controllabile a due livelli. I suoi stati ∣0⟩|0\rangle e ∣1⟩|1\rangle sono l'analogo di spin-up e spin-down, il gate che applichiamo per ruotarlo è l'analogo dell'impulso RF, e perde coerenza con i propri T1T_1 e T2T_2. Non si tratta solo di un confronto approssimativo; alcuni dei primi processori quantistici sperimentali, alla fine degli anni '90 e all'inizio degli anni 2000, usavano l'NMR.

In questo modulo userai un vero computer quantistico per eseguire gli stessi esperimenti di caratterizzazione che esegue uno spettroscopista NMR (o un ingegnere quantistico che calibra l'hardware):

  • Rabi / nutazione — pilotare lo spin e osservarlo ruotare. È così che si calibra una rotazione di π\pi o π/2\pi/2.

  • T1T_1 — rilassamento spin-reticolo, il decadimento di uno spin eccitato verso l'equilibrio.

  • T2T_2 — rilassamento spin-spin (sfasamento), misurato con un esperimento di Ramsey e rifocalizzato con un eco di Hahn.

  • Spettroscopia NMR (chemical shift e accoppiamento JJ) — simulare una piccola molecola su più qubit e leggerne lo spettro dei multipletti per ricavare la connettività tramite la regola n+1n+1.

Per ogni concetto forniamo prima la teoria minima, poi implementiamo l'esperimento in Qiskit seguendo il flusso di lavoro in quattro passaggi dei Qiskit patterns.

Teoria​

Particelle di spin 1/2 in un campo magnetico​

Un nucleo di spin 1/2 ha un momento magnetico μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}, dove γ\gamma è il rapporto giromagnetico. In un campo statico B0B_0 lungo zz, i due stati di spin si separano in energia (l'effetto Zeeman):

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 .

Il gap di energia definisce la frequenza di Larmor (di risonanza) ω0=γB0\omega_0 = \gamma B_0. Il momento magnetico precede attorno a B0B_0 a questa stessa frequenza.

Per ruotare lo spin applichiamo un campo RF oscillante a (o vicino a) ω0\omega_0. Nel sistema di riferimento che ruota alla frequenza di pilotaggio (il sistema rotante), un pilotaggio risonante ruota il vettore di Bloch attorno a un asse nel piano xx-yy. Quanto ruota (il flip angle) è determinato dall'intensità e dalla durata dell'impulso. Due scelte contano di più: un impulso di 90∘90^\circ porta uno spin +1/2+1/2 nel piano trasversale, e un impulso di 180∘180^\circ lo ruota completamente a −1/2-1/2.

Tradurre tra il gergo dell'NMR e quello del calcolo quantistico​

Un qubit è un sistema a due livelli, matematicamente identico a uno spin 1/2 in un campo magnetico — quindi, anche se lavoriamo su un processore quantistico anziché su un nucleo, la fisica è la stessa che sta alla base dell'NMR. Cambia solo il vocabolario. Questa tabella ti aiuta a parlare sia con i chimici fisici sia con gli specialisti di calcolo quantistico.

NMR / spin 1/2Processore quantistico (qubit)
Spin-up / spin-down∣0⟩\ket{0} / ∣1⟩\ket{1}
Gap di Zeeman ΔE=ℏω0\Delta E = \hbar\omega_0Energia di transizione del qubit ℏω01\hbar\omega_{01}
Frequenza di Larmor ω0\omega_0Frequenza del qubit ω01\omega_{01}
Impulso RF risonante (flip angle θ\theta)Gate di rotazione a singolo qubit RX(θ)R_X(\theta)
Impulso di 90∘90^\circ / 180∘180^\circRX(π/2)R_X(\pi/2) / RX(π)R_X(\pi)
Rilassamento spin-reticolo T1T_1Tempo di rilassamento energetico del qubit T1T_1
Rilassamento spin-spin T2T_2Tempo di sfasamento del qubit T2T_2
NutazioneOscillazioni di Rabi
Decadimento di induzione libera (FID), eco di HahnSequenze di Ramsey, sequenze di eco

Tutto ciò che segue è un esercizio di misura della colonna di destra e di lettura di essa come colonna di sinistra.

Configurare l'ambiente Qiskit​

Seguiamo ovunque il flusso di lavoro dei Qiskit patterns:

  1. Mappare il problema su un circuito quantistico.

  2. Ottimizzare il circuito per l'esecuzione (transpilare per il backend).

  3. Eseguire usando una primitiva di IBM Quantum.

  4. Post-elaborare e analizzare.

Per prima cosa carichiamo il servizio runtime e scegliamo il backend reale meno occupato. Se non hai ancora salvato le tue credenziali, consulta la pagina di documentazione su come salvare le tue credenziali di accesso.

# 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

Usa la cella seguente per passare tra hardware reale e un simulatore locale per l'intero notebook. Eseguire su hardware è lo scopo del modulo, ma se ci sono problemi di connettività o di utilizzo, il simulatore può fungere da riserva. Imposta USE_HARDWARE = True per eseguire sulla QPU selezionata sopra.

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

Esperimento: oscillazioni di Rabi (nutazione)​

Teoria​

Pilota uno spin in risonanza e il suo vettore di Bloch ruota con regolarità da un polo all'altro — nuta. Se applichiamo una rotazione di flip angle θ\theta attorno all'asse xx a un qubit che parte da ∣0⟩|0\rangle, la probabilità di trovarlo in ∣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).

Questa è la curva di Rabi. Il suo primo massimo è l'impulso π\pi (inversione completa, l'impulso di 180∘180^\circ); un impulso π/2\pi/2 è un quarto di giro (l'impulso di 90∘90^\circ che porta uno spin nel piano trasversale). In NMR questa curva è il modo in cui calibri i tuoi impulsi.

Avvertenza sperimentale

Un esperimento di Rabi letterale varia l'ampiezza o la durata del pilotaggio nel tempo. Ciò richiede il controllo a livello di impulso, che è stato rimosso in Qiskit v2.0. Qui invece variamo l'angolo di rotazione θ\theta di un gate RX(θ)R_X(\theta). La curva e l'identificazione di π\pi/π/2\pi/2 sono identiche; l'unica differenza è che scegliamo direttamente un angolo invece di calibrare la durata di un impulso. Su hardware con gate frazionari, RX(θ)R_X(\theta) per θ\theta arbitrario viene eseguito nativamente.

Una vista sulla sfera di Bloch​

L'animazione seguente mostra la nutazione: un pilotaggio risonante ruota con regolarità il vettore di Bloch attorno all'asse xx, portandolo da ∣0⟩|0\rangle verso ∣1⟩|1\rangle e di nuovo indietro. Questo è proprio il moto che la curva di Rabi misura.

Mappare il problema su un circuito​

Costruiamo un unico circuito parametrizzato, RX(θ)R_X(\theta) seguito da una misura, e lo valutiamo poi su molti angoli contemporaneamente.

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

Ottimizzare per l'esecuzione​

Transpila in un circuito Instruction Set Architecture (ISA) per il backend scelto.

rabi_isa = pm_q.run(rabi)

Eseguire​

Facciamo variare θ\theta da 00 a 4π4\pi (due cicli di Rabi completi). Passare un array di valori dei parametri come un singolo Primitive Unified Bloc (PUB) li esegue tutti in un solo 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-elaborare e analizzare​

Estrai P1P_1 a ogni angolo e confrontalo con 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

Verifica la tua comprensione​

Sulla curva che hai misurato, P1P_1 raggiunge per la prima volta il suo massimo all'impulso π\pi (θ=π\theta = \pi). Supponi ora che un dispositivo sia mal calibrato, così che un angolo nominale θ\theta sull'asse x produca in realtà una rotazione di 1.05 θ1.05\,\theta.

(a) A quale angolo nominale comparirebbe ora il primo massimo di P1P_1? (b) L'applicazione di un singolo impulso π\pi sarebbe un modo sensibile per rilevare questo errore del 5%?

Risposta

(a) Il massimo compare dove la rotazione effettiva è uguale a π\pi, cioè all'angolo nominale θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — un piccolo spostamento del picco verso sinistra.

(b) No. Un impulso π\pi nominale produce una rotazione effettiva di 1.05π1.05\pi, che dà P1=sin⁡2(1.05π/2)≈0.994P_1 = \sin^2(1.05\pi/2) \approx 0.994 invece di 11 — una variazione troppo piccola per essere vista in mezzo al rumore di misura. Per questo, per calibrare con precisione, gli spettroscopisti NMR amplificano l'errore applicando la rotazione molte volte (per esempio il metodo 360∘360^\circ, o un lungo treno di impulsi), in modo che un piccolo errore per singolo impulso si accumuli in una deviazione grande e misurabile. Torneremo su questa idea nei problemi di approfondimento.

Esperimento: T1T_1 (rilassamento spin-reticolo)​

Teoria​

Lasciato a se stesso, uno spin eccitato cede energia all'ambiente circostante (il "reticolo") e rilassa tornando verso l'equilibrio termico. La popolazione dello stato eccitato decade in modo esponenziale con il tempo di rilassamento spin-reticolo T1T_1:

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

Per un chimico, T1T_1 determina il ritardo di riciclo che devi aspettare tra una scansione e l'altra nell'NMR quantitativa, e fornisce informazioni sul moto molecolare e sulla dinamica. In un qubit è il tempo di rilassamento dell'energia, ed è uno dei due numeri principali usati per modellare gli effetti di decoerenza in un computer quantistico.

L'esperimento. Prepara ∣1⟩|1\rangle con una porta XX (un impulso π\pi), attendi un ritardo variabile τ\tau, poi misura. La frazione ancora in ∣1⟩|1\rangle traccia il decadimento.

Una visione sulla sfera di Bloch​

Il rilassamento T1T_1 riporta lo stato eccitato all'equilibrio, quindi il vettore di Bloch scivola da ∣1⟩|1\rangle di nuovo verso ∣0⟩|0\rangle lungo l'asse zz.

Costruire e traspilare una scansione dei ritardi​

Le durate dei ritardi devono essere valori concreti (vengono pianificate nel tempo dell'hardware), quindi costruiamo un circuito per ogni ritardo.

# 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

Esecuzione​

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

Analisi — adattare l'esponenziale​

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

Il tempo T1T_1 esatto varia da un esperimento all'altro e da un backend all'altro, ma dovresti ottenere un risultato dell'ordine di qualche centinaio di microsecondi.

Verifica la tua comprensione​

Nell'NMR quantitativa ti viene detto di aspettare circa 5 T15\,T_1 tra una scansione e l'altra. Usando la legge di decadimento, quale frazione della magnetizzazione di equilibrio si è recuperata dopo 5 T15\,T_1?

Risposta

Dopo 5 T15\,T_1, la deviazione residua dall'equilibrio è e−5≈0.0067e^{-5}\approx 0.0067, cioè circa il 99.3% è recuperato.

Esperimento: T2T_2, T2∗T_2^*​

Teoria​

T1T_1 misura la perdita di energia. T2T_2 misura la perdita di coerenza di fase nel piano trasversale. Possiamo osservarla con un esperimento di Ramsey: un impulso π/2\pi/2 ruota tutti gli spin fino all'equatore, tutti puntati nella stessa direzione. Hanno una precessione di Larmor, ma ciascuno precede a una frequenza leggermente diversa, quindi si aprono a ventaglio lungo l'equatore e il segnale trasversale netto decade, con un tempo di decadimento caratteristico T2∗T_2^*.

Nota a margine per i lettori di informatica quantistica: L'immagine del ventaglio mostra un modo di perdere coerenza: una distribuzione di fasi definite che si annullano in media. Vale la pena sapere che non è l'unico meccanismo. La coerenza può anche andare persa perché la fase si entangla con l'ambiente, per cui non è più affatto un valore definito posseduto dal sistema. Non puoi distinguere i due meccanismi semplicemente osservando il decadimento del segnale, quindi l'immagine del ventaglio è una buona intuizione. Fai attenzione a non leggerla come la storia completa o letterale.

Due effetti sfasano uno spin:

  • La decoerenza irreversibile (o effettivamente irreversibile) dovuta all'accoppiamento incontrollato con altri gradi di libertà, caratterizzata dal vero T2T_2. Nell'NMR, questa deriva in genere dal moto molecolare che modula gli accoppiamenti tra gli spin. Nel calcolo quantistico corrisponde agli accoppiamenti indesiderati tra i qubit e l'ambiente.
  • Gli scostamenti di frequenza reversibili, statici o lentamente variabili, come la disomogeneità del campo, la variazione di frequenza da qubit a qubit e gli errori di calibrazione. Combinati con il vero T2T_2, producono il tempo di decadimento apparente più breve T2∗T_2^* osservato in un esperimento di Ramsey.

Come recuperiamo allora il vero T2T_2 se un esperimento di Ramsey ci dà informazioni solo su T2∗T_2^*? Il secondo esperimento, chiamato eco di Hahn, cancella tutto il rumore del secondo tipo per trovare il vero T2T_2. Inizia come un esperimento di Ramsey, con un impulso π/2\pi/2, poi gli spin si aprono a ventaglio come prima. Dopo un certo tempo τ\tau viene applicato un impulso π\pi. L'impulso inverte l'accumulo di fase causato dagli scostamenti di frequenza statici, così gli spin si rifocalizzano dopo un secondo ritardo della stessa durata, τ\tau.

T2∗T_2^* e T2T_2 sono entrambi numeri importanti da conoscere sia in ambito chimico sia nel calcolo quantistico. In ambito chimico, T2∗T_2^* determina la larghezza di riga in un esperimento NMR standard (vedrai cosa significa in una sezione successiva). T2T_2 può dirci molto sulle molecole, come il moto molecolare, e una variazione di T2T_2 può indicare che una molecola si è legata a qualcosa di più grande. Nel calcolo quantistico, in genere vogliamo che sia T2T_2 sia T2∗T_2^* siano il più lunghi possibile, così da avere più tempo per eseguire esperimenti prima che la decoerenza sopraffaccia il sistema. Un'intera famiglia di tecniche basate sull'eco di Hahn allunga questo tempo cancellando il rumore reversibile, così che il circuito sia idealmente limitato dal vero T2T_2 e non da T2∗T_2^*. Questo si chiama "disaccoppiamento dinamico".

Implementazione​

Quando implementiamo gli esperimenti di Ramsey e dell'eco di Hahn su un qubit, la lettura è leggermente diversa rispetto al caso NMR. Nell'NMR puoi osservare direttamente i campi oscillanti indotti dagli spin che precedono alla frequenza di Larmor nel piano trasversale. Ma con un qubit possiamo misurare solo la sua proiezione lungo l'asse ZZ, cioè se è ∣0⟩\vert 0 \rangle o ∣1⟩\vert 1 \rangle. Pertanto, per entrambi gli esperimenti, terminiamo con un ulteriore impulso π/2\pi/2 seguito da una misura. Questo impulso π/2\pi/2 traduce l'informazione di fase del qubit in una popolazione.

Di seguito eseguiamo ciascun esperimento per determinare T2∗T_2^* e T2T_2.

Ramsey (T2∗T_2^{*}): due impulsi π/2\pi/2 con tempo di ritardo τ\tau​

Il primo impulso π/2\pi/2 porta il vettore di Bloch all'equatore e il successivo traduce la fase del vettore di Bloch in una popolazione. Poiché la fase cambia nel tempo mentre il vettore di Bloch precede, vediamo un andamento sinusoidale nella popolazione in funzione del tempo di precessione τ\tau. Man mano che il qubit perde coerenza di fase dovremmo vedere decadere l'ampiezza di questa curva sinusoidale.

Qualche sottigliezza in più per i lettori di informatica quantistica: Negli esperimenti di calcolo quantistico, le oscillazioni che vediamo non sono in realtà alla frequenza di Larmor, che è nell'intervallo dei GHz e troppo veloce perché possiamo risolverla. Gli esperimenti quantistici operano nel sistema di riferimento rotante: un sistema che ruota insieme al qubit a una frequenza (quasi) pari a quella di Larmor, così la rapida precessione è invisibile e il vettore di Bloch appare quasi fermo. Per vedere le frange, impostiamo deliberatamente il nostro sistema rotante leggermente sfasato rispetto alla frequenza di Larmor. Il vettore di Bloch sembra allora precedere a quella piccola differenza di frequenza, e otteniamo un segnale oscillante alla frequenza di detuning. Di seguito non fissiamo il detuning a un solo valore; lo scegliamo in modo che circa una dozzina di frange coprano la finestra di misura (che dimensioniamo sul tempo di coerenza del qubit), il che rende l'oscillazione facile da campionare senza aliasing. Un'ulteriore complicazione: in Qiskit non abbiamo il controllo a livello di impulso necessario per cambiare la frequenza dell'impulso (che è ciò che determina la frequenza del sistema rotante). Aggiungiamo invece a mano un detuning effettivo, usandolo per impostare la fase del secondo impulso di Ramsey. È un po' artificioso, ma il risultato è lo stesso e il decadimento che osserviamo è comunque il genuino T2∗T_2^*.

Una visione sulla sfera di Bloch​

Un impulso π/2\pi/2 porta gli spin all'equatore; ciascuno poi precede a una velocità leggermente diversa e si aprono a ventaglio, quindi il vettore netto (rosso) si accorcia man mano che si sfasano — questo decadimento è 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

Eco di Hahn (T2T_2): aggiungere un impulso π\pi di rifocalizzazione a metà​

Una visione sulla sfera di Bloch​

L'eco di Hahn: gli spin si aprono a ventaglio come prima, poi un impulso π\pi li ribalta, così la loro precessione continua li rifocalizza di nuovo insieme (il vettore rosso ricresce), recuperando la coerenza persa per sfasamento statico.

# 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

Analisi: adattare e confrontare i due decadimenti​

# 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

Qui vediamo decadere sia l'oscillazione di Ramsey sia il segnale dell'eco di Hahn, come previsto. Le scale temporali associate a ciascuno di questi decadimenti sono rispettivamente T2∗T_2^* e T2T_2.

Verifica la tua comprensione​

Di solito dovresti trovare T2≳T2∗T_2 \gtrsim T_2^{*}. Spiega perché in una frase, e indica quale effetto fisico l'impulso π\pi dell'eco di Hahn rimuove, e quale invece non può rimuovere.

Risposta

T2∗T_2^{*} include sia lo sfasamento irreversibile sia la dispersione statica reversibile, quindi decade più rapidamente; l'impulso π\pi dell'eco rifocalizza lo sfasamento statico e reversibile (disomogeneità del campo / scostamenti di frequenza fissi), lasciando solo lo sfasamento spin-spin irreversibile, che è il vero T2T_2. L'eco non può annullare le fluttuazioni che cambiano tra le due metà della sequenza.

Spettroscopia NMR di una molecola: chemical shift e accoppiamento JJ​

Da un singolo spin a una molecola​

Gli esperimenti finora hanno caratterizzato un singolo spin isolato. L'NMR chimica reale legge una molecola (molti spin accoppiati contemporaneamente) e usa lo spettro risultante per dedurre caratteristiche della struttura della molecola. Questo può dirci qualcosa su due effetti:

  • Chemical shift. Ogni nucleo chimicamente distinto precede a una frequenza νi\nu_i leggermente diversa, perché gli elettroni locali schermano il campo applicato in misura diversa. Le posizioni dei picchi identificano quindi gli ambienti chimici (per esempio un protone di un CH3_3 rispetto a uno aromatico).

  • Accoppiamento scalare JJ. Un'interazione attraverso i legami tra due spin divide ogni picco in un multipletto. Un nucleo con nn vicini equivalenti si divide in n+1n+1 righe con intensità binomiali, la regola n+1n+1. Il pattern di suddivisione rivela la connettività (quali nuclei sono legati), e la spaziatura è la costante di accoppiamento JJ.

L'esperimento​

Per modellare questo nel nostro computer quantistico, dobbiamo implementare un hamiltoniano di spin sui qubit. Nel limite di accoppiamento debole e campo alto (differenze di chemical shift ≫\gg accoppiamenti), le parti XXXX e YYYY dell'accoppiamento si annullano in media, lasciando solo ZZZZ. Quindi l'hamiltoniano è (in unità di frequenza angolare, ℏ=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}}.

Ogni nucleo è un qubit: il chemical shift è una rotazione ZZ a singolo qubit, e l'accoppiamento è un'interazione ZZZZ a due qubit.

L'esperimento procede come segue. Un impulso π/2\pi/2 porta ogni spin nel piano trasversale, e gli spin precedono e si accoppiano per un tempo tt. Registriamo la magnetizzazione trasversale complessa s(t)=⟨X⟩+i⟨Y⟩s(t) = \langle X\rangle + i\langle Y\rangle sommata su tutti gli spin. Come nell'esperimento di Ramsey visto sopra, questa magnetizzazione trasversale traccia una sinusoide smorzata in funzione del tempo di precessione tt. Quando calcoliamo la trasformata di Fourier del segnale, otteniamo uno spettro con picchi al chemical shift νi\nu_i suddivisi dall'accoppiamento scalare JijJ_{ij}.

nota

Simuliamo l'evoluzione della molecola mappando il tempo tt su angoli di rotazione: un tt maggiore significa rotazioni ZZ e ZZZZ più grandi, non un circuito più lungo o più profondo. Quindi, anche se lo stato simulato avanza effettivamente con tt (è questo che fa oscillare il decadimento di induzione libera (FID)), i qubit fisici non restano mai inattivi più a lungo e non c'è alcuna decoerenza dipendente da tt. Il FID simulato quindi non decade mai da solo. Per un FID ideale infinitamente lungo le righe spettrali sarebbero infinitamente nitide; la nostra finestra di campionamento finita dà loro invece una risoluzione finita e può introdurre dispersione spettrale. Le righe NMR reali hanno una larghezza finita determinata da T2∗T_2^*. La reintroduciamo moltiplicando il FID per una finestra e−t/T2∗e^{-t/T_2^{*}} prima della trasformata di Fourier. Questa si chiama apodizzazione di allargamento di riga, che gli spettroscopisti applicano nell'elaborazione per impostare la larghezza di riga (scambiando risoluzione con rapporto segnale-rumore). (Una cella opzionale nell'Appendice di questo modulo mostra un'alternativa trotterizzata, in cui il decadimento deriva invece dalla decoerenza reale che cresce con la profondità del circuito.)

Il sistema AX​

Iniziamo dal cosiddetto sistema AX, una molecola costituita da due spin accoppiati e chimicamente distinti. Le etichette A e X indicano i due ambienti di spin chimicamente distinti. Le lettere sono arbitrarie, ma il fatto che siano lontane nell'alfabeto illustra la loro dissomiglianza chimica.

Mappare e ottimizzare​

Scriviamo una funzione che, dati gli offset di chemical shift νi\nu_i e gli accoppiamenti JijJ_{ij}, restituisce un circuito di preparazione dello stato parametrizzato. L'impulso π/2\pi/2 porta gli spin all'equatore, poi le rotazioni ZZ simulano l'evoluzione dello stato sotto i termini di chemical shift e di accoppiamento JJ. Ogni νi\nu_i è un piccolo offset di precessione in Hz rispetto a un riferimento scelto, o portante del sistema rotante. Qui lo impostiamo manualmente; in una molecola reale, la schermatura elettronica cambia il campo magnetico locale e sposta la risonanza di ciascun nucleo rispetto a quel riferimento. Questo è modellato dalle porte RZR_Z a singolo qubit, una per spin, e ogni νi\nu_i fissa dove si trova il picco di quello spin nello spettro. La porta ZZZZ a due qubit applica l'accoppiamento JijJ_{ij}, che divide ogni picco in un multipletto e determina la spaziatura tra le sue righe.

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

Esecuzione e post-elaborazione​

Valutiamo il circuito su una griglia di tempi di evoluzione, e l'Estimator restituisce ⟨X⟩\langle X\rangle e ⟨Y⟩\langle Y\rangle per ciascuno. Li combiniamo nel FID complesso ⟨X⟩+i⟨Y⟩\langle X\rangle + i\langle Y\rangle, applichiamo la finestra di apodizzazione e calcoliamo la trasformata di Fourier del risultato per ottenere lo spettro.

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

Estensione: un sistema AX2_2 e la regola n+1n+1​

Aggiungiamo ora un secondo vicino equivalente. In un sistema AX2_2 uno spin A si accoppia, con lo stesso JJ, a due spin X equivalenti. (Ricorda che X è solo un'etichetta per i due spin equivalenti; non ha niente a che fare con gli operatori di Pauli XX. Ogni accoppiamento qui sotto è comunque ZZZZ.) Lo spin A vede allora quattro configurazioni dei vicini (↑↑, ↑↓, ↓↑, ↓↓)(\uparrow\uparrow,\ \uparrow\downarrow,\ \downarrow\uparrow,\ \downarrow\downarrow); le due miste coincidono, quindi A si divide in un tripletto 1:2:1 (n=2⇒n+1=3n=2 \Rightarrow n+1 = 3 righe). Ogni spin X vede solo A e resta un doppietto. Gli spin equivalenti non si dividono a vicenda, quindi non includiamo alcun accoppiamento X–X.

Una molecola reale con questo pattern è il 1,1,2-tricloroetano (Cl2_2CH–CH2_2Cl). Quando si osservano gli spin dell'idrogeno (la forma più comune di NMR), il singolo protone di idrogeno in CH svolge il ruolo di A, e i due protoni equivalenti di CH2_2 sono i due spin X. Ora basta contare i vicini: il protone CH ha due vicini (la coppia CH2_2), quindi il suo segnale si divide in un tripletto 1:2:1 (n=2n=2); ogni protone CH2_2 ha un solo vicino (il CH), quindi il segnale di CH2_2 è un doppietto 1:1 (n=1n=1). Nota che il gruppo con meno protoni (CH) mostra più righe; il numero di righe riflette quanti vicini ha un segnale, non quanti protoni lo condividono.

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

Di seguito simuliamo un sistema AX2_2 con tre qubit.

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

Verifica la tua comprensione​

Leggi ogni domanda, pensa alla tua risposta, poi clicca sul triangolo per rivelare la soluzione.

Nello spettro AX i due doppietti sono separati (da centro a centro) di 12 Hz, e le due righe all'interno di ciascun doppietto distano 2 Hz. Quale numero è la differenza di chemical shift e quale è la costante di accoppiamento JJ? Se passassi a uno spettrometro con un campo magnetico doppio, quale dei due cambierebbe?

Risposta

La separazione centro-centro di 12 Hz è la differenza di chemical shift (ν1−ν2\nu_1-\nu_2); la spaziatura di 2 Hz tra le righe è la costante di accoppiamento JJ. I chemical shift derivano dalla schermatura di B0B_0, quindi scalano con il campo. A campo doppio la separazione raddoppia a 24 Hz. L'accoppiamento JJ è una proprietà molecolare attraverso i legami ed è indipendente dal campo, quindi resta 2 Hz. I chimici distinguono lo shift dall'accoppiamento osservando come una suddivisione scala con il campo.

Usando la regola n+1n+1, prevedi il multipletto di A se venisse aggiunto un terzo spin X equivalente (un sistema AX3_3), e spiega perché i due spin X equivalenti in AX2_2 non si dividono a vicenda.

Risposta

Con n=3n=3 vicini equivalenti, A diventa un quartetto 1:3:3:1. Gli spin equivalenti non si dividono a vicenda perché il loro accoppiamento sposta tutti i livelli di energia rilevanti della stessa quantità, quindi le differenze tra quei livelli — le frequenze che vediamo — non cambiano.

Conclusione​

Abbiamo usato un vero computer quantistico per esplorare come gli spin nucleari precedono e decoerono in un esperimento NMR, e come interpretare gli spettri risultanti. Vale la pena fare un passo indietro per rivedere perché tutto questo sia stato possibile e perché sia più di una semplice curiosità.

Un qubit e un nucleo di spin 1/2 sono, matematicamente, lo stesso oggetto: sistemi quantistici a due livelli che vivono sulla sfera di Bloch, ruotati da impulsi risonanti, che precedono attorno a un asse e perdono coerenza a causa dell'ambiente circostante. L'intero vocabolario di questo modulo (la frequenza di Larmor, il sistema di riferimento rotante, T1T_1, T2T_2 e T2∗T_2^*, le frange di Ramsey, l'eco di Hahn) appartiene in egual misura all'NMR e al calcolo quantistico. Ciò che un chimico chiama chemical shift, un ingegnere quantistico lo chiama rotazione ZZ a singolo qubit; ciò che un chimico chiama accoppiamento JJ, un ingegnere quantistico lo chiama interazione ZZZZ. I due campi descrivono la stessa fisica con parole diverse.

Questa connessione è storica, non solo concettuale: l'NMR è stata una delle prime piattaforme a eseguire veri algoritmi quantistici. Nel 2001, una molecola in soluzione aveva eseguito l'algoritmo di Shor per fattorizzare 15, con ogni spin nucleare che fungeva da qubit. L'NMR ha anche consegnato al calcolo quantistico gran parte del suo corredo di strumenti di controllo. L'eco di spin, i treni di impulsi CPMG, gli impulsi compositi e il disaccoppiamento dinamico sono stati tutti inventati dagli spettroscopisti decenni prima che i qubit ne avessero bisogno per combattere la decoerenza. C'è una simmetria appagante in ciò che abbiamo appena fatto: il campo che ha dato vita ai primi computer quantistici è ora qualcosa che un computer quantistico può simulare.

Riepilogo e domande​

Concetti fondamentali​

  • Un qubit è un sistema a due livelli ingegnerizzato equivalente a uno spin 1/2; i suoi ∣0⟩/∣1⟩|0\rangle/|1\rangle, la frequenza, le porte, T1T_1 e T2T_2 sono gli analoghi diretti delle grandezze NMR.

  • Un impulso risonante ruota il vettore di Bloch; l'angolo di flip determina la rotazione, e la curva di Rabi/nutazione P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) identifica le rotazioni π\pi e π/2\pi/2.

  • T1T_1 è il rilassamento dell'energia (spin-reticolo); T2T_2 è il rilassamento di fase (spin-spin); T2∗T_2^{*} include in aggiunta lo sfasamento statico reversibile che un'eco di Hahn rifocalizza.

  • Una molecola è un insieme di spin accoppiati: il chemical shift determina la posizione di ciascun picco, e l'accoppiamento JJ divide i picchi in multipletti secondo la regola n+1n+1, che insieme codificano l'ambiente chimico e la connettività.

Vero / falso​

  1. V/F La frequenza di Larmor è il gap di energia tra i due stati di spin diviso per ℏ\hbar.

  2. V/F Un impulso di 90∘90^\circ (π/2\pi/2) inverte completamente uno spin all'equilibrio.

  3. V/F T2∗T_2^{*} è generalmente più lungo di T2T_2.

  4. V/F L'eco di Hahn rifocalizza lo sfasamento spin-spin irreversibile.

  5. V/F In un sistema AX2_2 il nucleo A appare come un tripletto 1:2:1.

  6. V/F La costante di accoppiamento JJ raddoppia quando raddoppi il campo magnetico dello spettrometro.

  7. V/F Rimuovere Qiskit Pulse rende impossibile misurare T1T_1 sull'hardware attuale.

Risposta breve​

  1. Spiega in due frasi perché un singolo impulso π\pi è un modo poco efficace per rilevare un errore del 5%5\% nell'angolo di flip, e come il concatenare impulsi risolve il problema.

  2. Un campione mostra righe NMR larghe. In termini di T2T_2, cosa ti dice questo, e qual è la grandezza analoga per un qubit?

  3. Indica l'analogo per un qubit di ciascuno: (a) il ritardo di riciclo, (b) l'impulso di 90∘90^\circ, (c) il chemical shift.

  4. Usando la regola n+1n+1, spiega perché un gruppo etile (CH3_3CH2_2–) mostra un tripletto 1:2:1 e un quartetto 1:3:3:1, e identifica quale gruppo produce quale multipletto.

Problemi di approfondimento​

  1. Treno di Carr-Purcell. Modifica l'eco di Hahn inserendo nn impulsi π\pi equispaziati (un treno in stile CPMG) e misura T2T_2 in funzione di nn. Discuti perché più impulsi di rifocalizzazione possono allungare il T2T_2 apparente.

  2. Amplificazione dell'errore di impulso. Costruisci un circuito che applica RX(π)R_X(\pi)  n\,n volte a ∣0⟩|0\rangle e traccia P1P_1 in funzione di nn per un angolo deliberatamente sbagliato RX(1.05π)R_X(1.05\pi). Mostra come l'errore diventa visibile al crescere di nn. Suggerimento: inserisci una barriera dopo ogni rotazione; altrimenti il transpiler potrebbe combinare le rotazioni ripetute in un'unica porta equivalente.

  3. Gruppo etile. Usando fid_circuit, costruisci lo spettro protonico di un frammento etile (CH3_3–CH2_2): cinque qubit, con i tre protoni CH3_3 che condividono un chemical shift e i due protoni CH2_2 un altro, e ogni coppia CH3_3–CH2_2 accoppiata dallo stesso JJ (nessun accoppiamento all'interno di un gruppo equivalente). Prevedi il pattern con la regola n+1n+1, poi simulalo; dovresti ritrovare un quartetto 1:3:3:1 (CH2_2) e un tripletto 1:2:1 (CH3_3).

Appendice (facoltativa): evoluzione trotterizzata — lasciare che sia la decoerenza a determinare la larghezza di riga​

Nella sezione Spettroscopia NMR di una molecola, l'evoluzione e−iHte^{-iHt} viene applicata in un singolo strato la cui profondità non cresce con tt, quindi il FID non decade da solo e imponiamo la larghezza di riga con la finestra di apodizzazione e−t/T2e^{-t/T_2}. Questa cella facoltativa adotta l'approccio opposto. Suddivide l'evoluzione in pp passi di Trotter e inserisce una barriera dopo ogni passo (in modo che il transpiler non possa riunirli in un unico strato). Il punto temporale kk usa kk passi, quindi la profondità del circuito cresce con il tempo.

Poiché ogni termine di HH commuta, questo è esatto: su un simulatore privo di rumore dà gli stessi picchi nitidi. Ma su un backend con modello di rumore o su hardware reale l'errore di porta che si accumula fa decadere il FID da solo, ed è quel decadimento a determinare la larghezza di riga. Questa è l'idea T2↔T_2\leftrightarrowlarghezza di riga della sezione esperimento T2T_2, T2∗T_2^*, che ora nasce dalla decoerenza reale invece che da una finestra di elaborazione. In questo modo eseguiamo sia il sistema AX (due doppietti) sia il sistema AX2_2 (un tripletto 1:2:1 più un doppietto).

Due note pratiche da considerare. Primo, il costo è la profondità: il circuito AX più profondo usa ∼ ⁣47\sim\!47 porte a due qubit e la versione AX2_2 circa il doppio, quindi entrambi sono molto più rumorosi e costosi della versione esatta. In secondo luogo, ogni sistema di spin viene comunque inviato come un unico job, e ogni punto temporale riutilizza lo stesso layout di qubit fisici selezionato nella sezione di spettroscopia. Se USE_BATCH=True, questi job usano anche il Batch facoltativo valido per tutto il notebook.

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