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:
qiskitv2.1 o successivaqiskit-ibm-runtimev0.40.1 o successivaqiskit-aerv0.17 o successivascipyematplotlib(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, e .
Un qubit superconduttore è una versione ingegnerizzata dello stesso oggetto: un sistema quantistico controllabile a due livelli. I suoi stati e 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 e . 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 o .
-
— rilassamento spin-reticolo, il decadimento di uno spin eccitato verso l'equilibrio.
-
— rilassamento spin-spin (sfasamento), misurato con un esperimento di Ramsey e rifocalizzato con un eco di Hahn.
-
Spettroscopia NMR (chemical shift e accoppiamento ) — simulare una piccola molecola su più qubit e leggerne lo spettro dei multipletti per ricavare la connettività tramite la regola .
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 , dove è il rapporto giromagnetico. In un campo statico lungo , i due stati di spin si separano in energia (l'effetto Zeeman):
Il gap di energia definisce la frequenza di Larmor (di risonanza) . Il momento magnetico precede attorno a a questa stessa frequenza.
Per ruotare lo spin applichiamo un campo RF oscillante a (o vicino a) . 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 -. Quanto ruota (il flip angle) è determinato dall'intensità e dalla durata dell'impulso. Due scelte contano di più: un impulso di porta uno spin nel piano trasversale, e un impulso di lo ruota completamente a .
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/2 | Processore quantistico (qubit) |
|---|---|
| Spin-up / spin-down | / |
| Gap di Zeeman | Energia di transizione del qubit |
| Frequenza di Larmor | Frequenza del qubit |
| Impulso RF risonante (flip angle ) | Gate di rotazione a singolo qubit |
| Impulso di / | / |
| Rilassamento spin-reticolo | Tempo di rilassamento energetico del qubit |
| Rilassamento spin-spin | Tempo di sfasamento del qubit |
| Nutazione | Oscillazioni di Rabi |
| Decadimento di induzione libera (FID), eco di Hahn | Sequenze 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:
-
Mappare il problema su un circuito quantistico.
-
Ottimizzare il circuito per l'esecuzione (transpilare per il backend).
-
Eseguire usando una primitiva di IBM Quantum.
-
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 attorno all'asse a un qubit che parte da , la probabilità di trovarlo in è:
Questa è la curva di Rabi. Il suo primo massimo è l'impulso (inversione completa, l'impulso di ); un impulso è un quarto di giro (l'impulso di che porta uno spin nel piano trasversale). In NMR questa curva è il modo in cui calibri i tuoi impulsi.
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 di un gate . La curva e l'identificazione di / sono identiche; l'unica differenza è che scegliamo direttamente un angolo invece di calibrare la durata di un impulso. Su hardware con gate frazionari, per 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 , portandolo da verso 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, 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")
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 da a (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 a ogni angolo e confrontalo con .
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()

Verifica la tua comprensione
Sulla curva che hai misurato, raggiunge per la prima volta il suo massimo all'impulso (). Supponi ora che un dispositivo sia mal calibrato, così che un angolo nominale sull'asse x produca in realtà una rotazione di .
(a) A quale angolo nominale comparirebbe ora il primo massimo di ? (b) L'applicazione di un singolo impulso sarebbe un modo sensibile per rilevare questo errore del 5%?
Risposta
(a) Il massimo compare dove la rotazione effettiva è uguale a , cioè all'angolo nominale — un piccolo spostamento del picco verso sinistra.
(b) No. Un impulso nominale produce una rotazione effettiva di , che dà invece di — 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 , 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: (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 :
Per un chimico, 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 con una porta (un impulso ), attendi un ritardo variabile , poi misura. La frazione ancora in traccia il decadimento.
Una visione sulla sfera di Bloch
Il rilassamento riporta lo stato eccitato all'equilibrio, quindi il vettore di Bloch scivola da di nuovo verso lungo l'asse .
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")
Measured T1 = 225.3 microseconds
Il tempo 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 tra una scansione e l'altra. Usando la legge di decadimento, quale frazione della magnetizzazione di equilibrio si è recuperata dopo ?
Risposta
Dopo , la deviazione residua dall'equilibrio è , cioè circa il 99.3% è recuperato.
Esperimento: ,
Teoria
misura la perdita di energia. misura la perdita di coerenza di fase nel piano trasversale. Possiamo osservarla con un esperimento di Ramsey: un impulso 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 .
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 . 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 , producono il tempo di decadimento apparente più breve osservato in un esperimento di Ramsey.
Come recuperiamo allora il vero se un esperimento di Ramsey ci dà informazioni solo su ? Il secondo esperimento, chiamato eco di Hahn, cancella tutto il rumore del secondo tipo per trovare il vero . Inizia come un esperimento di Ramsey, con un impulso , poi gli spin si aprono a ventaglio come prima. Dopo un certo tempo viene applicato un impulso . 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, .
e sono entrambi numeri importanti da conoscere sia in ambito chimico sia nel calcolo quantistico. In ambito chimico, determina la larghezza di riga in un esperimento NMR standard (vedrai cosa significa in una sezione successiva). può dirci molto sulle molecole, come il moto molecolare, e una variazione di può indicare che una molecola si è legata a qualcosa di più grande. Nel calcolo quantistico, in genere vogliamo che sia sia 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 e non da . 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 , cioè se è o . Pertanto, per entrambi gli esperimenti, terminiamo con un ulteriore impulso seguito da una misura. Questo impulso traduce l'informazione di fase del qubit in una popolazione.
Di seguito eseguiamo ciascun esperimento per determinare e .
Ramsey (): due impulsi con tempo di ritardo
Il primo impulso 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 . 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 .
Una visione sulla sfera di Bloch
Un impulso 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 è .
# 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)
Eco di Hahn (): aggiungere un impulso 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 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
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")

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 e .
Verifica la tua comprensione
Di solito dovresti trovare . Spiega perché in una frase, e indica quale effetto fisico l'impulso dell'eco di Hahn rimuove, e quale invece non può rimuovere.
Risposta
include sia lo sfasamento irreversibile sia la dispersione statica reversibile, quindi decade più rapidamente; l'impulso 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 . L'eco non può annullare le fluttuazioni che cambiano tra le due metà della sequenza.
Spettroscopia NMR di una molecola: chemical shift e accoppiamento
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 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 CH rispetto a uno aromatico).
-
Accoppiamento scalare . Un'interazione attraverso i legami tra due spin divide ogni picco in un multipletto. Un nucleo con vicini equivalenti si divide in righe con intensità binomiali, la regola . Il pattern di suddivisione rivela la connettività (quali nuclei sono legati), e la spaziatura è la costante di accoppiamento .
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 accoppiamenti), le parti e dell'accoppiamento si annullano in media, lasciando solo . Quindi l'hamiltoniano è (in unità di frequenza angolare, ):
Ogni nucleo è un qubit: il chemical shift è una rotazione a singolo qubit, e l'accoppiamento è un'interazione a due qubit.
L'esperimento procede come segue. Un impulso porta ogni spin nel piano trasversale, e gli spin precedono e si accoppiano per un tempo . Registriamo la magnetizzazione trasversale complessa 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 . Quando calcoliamo la trasformata di Fourier del segnale, otteniamo uno spettro con picchi al chemical shift suddivisi dall'accoppiamento scalare .
Simuliamo l'evoluzione della molecola mappando il tempo su angoli di rotazione: un maggiore significa rotazioni e più grandi, non un circuito più lungo o più profondo. Quindi, anche se lo stato simulato avanza effettivamente con (è 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 . 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 . La reintroduciamo moltiplicando il FID per una finestra 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 e gli accoppiamenti , restituisce un circuito di preparazione dello stato parametrizzato. L'impulso porta gli spin all'equatore, poi le rotazioni simulano l'evoluzione dello stato sotto i termini di chemical shift e di accoppiamento . Ogni è 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 a singolo qubit, una per spin, e ogni fissa dove si trova il picco di quello spin nello spettro. La porta a due qubit applica l'accoppiamento , 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 e per ciascuno. Li combiniamo nel FID complesso , 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)}),
)

Expected AX lines (Hz): [-7.0, -5.0, 5.0, 7.0]
Estensione: un sistema AX e la regola
Aggiungiamo ora un secondo vicino equivalente. In un sistema AX uno spin A si accoppia, con lo stesso , 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 . Ogni accoppiamento qui sotto è comunque .) Lo spin A vede allora quattro configurazioni dei vicini ; le due miste coincidono, quindi A si divide in un tripletto 1:2:1 ( 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 (ClCH–CHCl). 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 CH sono i due spin X. Ora basta contare i vicini: il protone CH ha due vicini (la coppia CH), quindi il suo segnale si divide in un tripletto 1:2:1 (); ogni protone CH ha un solo vicino (il CH), quindi il segnale di CH è un doppietto 1: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.
Di seguito simuliamo un sistema AX 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 ? 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 (); la spaziatura di 2 Hz tra le righe è la costante di accoppiamento . I chemical shift derivano dalla schermatura di , quindi scalano con il campo. A campo doppio la separazione raddoppia a 24 Hz. L'accoppiamento è 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 , prevedi il multipletto di A se venisse aggiunto un terzo spin X equivalente (un sistema AX), e spiega perché i due spin X equivalenti in AX non si dividono a vicenda.
Risposta
Con 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, , e , 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 a singolo qubit; ciò che un chimico chiama accoppiamento , un ingegnere quantistico lo chiama interazione . 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 , la frequenza, le porte, e 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 identifica le rotazioni e .
-
è il rilassamento dell'energia (spin-reticolo); è il rilassamento di fase (spin-spin); 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 divide i picchi in multipletti secondo la regola , che insieme codificano l'ambiente chimico e la connettività.
Vero / falso
-
V/F La frequenza di Larmor è il gap di energia tra i due stati di spin diviso per .
-
V/F Un impulso di () inverte completamente uno spin all'equilibrio.
-
V/F è generalmente più lungo di .
-
V/F L'eco di Hahn rifocalizza lo sfasamento spin-spin irreversibile.
-
V/F In un sistema AX il nucleo A appare come un tripletto 1:2:1.
-
V/F La costante di accoppiamento raddoppia quando raddoppi il campo magnetico dello spettrometro.
-
V/F Rimuovere Qiskit Pulse rende impossibile misurare sull'hardware attuale.
Risposta breve
-
Spiega in due frasi perché un singolo impulso è un modo poco efficace per rilevare un errore del nell'angolo di flip, e come il concatenare impulsi risolve il problema.
-
Un campione mostra righe NMR larghe. In termini di , cosa ti dice questo, e qual è la grandezza analoga per un qubit?
-
Indica l'analogo per un qubit di ciascuno: (a) il ritardo di riciclo, (b) l'impulso di , (c) il chemical shift.
-
Usando la regola , spiega perché un gruppo etile (CHCH–) mostra un tripletto 1:2:1 e un quartetto 1:3:3:1, e identifica quale gruppo produce quale multipletto.
Problemi di approfondimento
-
Treno di Carr-Purcell. Modifica l'eco di Hahn inserendo impulsi equispaziati (un treno in stile CPMG) e misura in funzione di . Discuti perché più impulsi di rifocalizzazione possono allungare il apparente.
-
Amplificazione dell'errore di impulso. Costruisci un circuito che applica volte a e traccia in funzione di per un angolo deliberatamente sbagliato . Mostra come l'errore diventa visibile al crescere di . Suggerimento: inserisci una barriera dopo ogni rotazione; altrimenti il transpiler potrebbe combinare le rotazioni ripetute in un'unica porta equivalente.
-
Gruppo etile. Usando
fid_circuit, costruisci lo spettro protonico di un frammento etile (CH–CH): cinque qubit, con i tre protoni CH che condividono un chemical shift e i due protoni CH un altro, e ogni coppia CH–CH accoppiata dallo stesso (nessun accoppiamento all'interno di un gruppo equivalente). Prevedi il pattern con la regola , poi simulalo; dovresti ritrovare un quartetto 1:3:3:1 (CH) e un tripletto 1:2:1 (CH).
Appendice (facoltativa): evoluzione trotterizzata — lasciare che sia la decoerenza a determinare la larghezza di riga
Nella sezione Spettroscopia NMR di una molecola, l'evoluzione viene applicata in un singolo strato la cui profondità non cresce con , quindi il FID non decade da solo e imponiamo la larghezza di riga con la finestra di apodizzazione . Questa cella facoltativa adotta l'approccio opposto. Suddivide l'evoluzione in 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 usa passi, quindi la profondità del circuito cresce con il tempo.
Poiché ogni termine di 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 larghezza di riga della sezione esperimento , , 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 AX (un tripletto 1:2:1 più un doppietto).
Due note pratiche da considerare. Primo, il costo è la profondità: il circuito AX più profondo usa porte a due qubit e la versione AX 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()