Saltar al contenido principal
Este proyecto está en fase beta. ¿Encontraste un error? Abre un issue en GitHub. ¿Tienes una pregunta o una idea? Pregunta en Discussions — ¡nos encantaría tu opinión!
¿Cómo es la calidad de la traducción en esta página?

Resonancia magnética nuclear con Qiskit

Para este módulo de Qiskit in Classrooms, los estudiantes deben tener un entorno de Python funcional con los siguientes paquetes instalados:

  • qiskit v2.1 o posterior
  • qiskit-ibm-runtime v0.40.1 o posterior
  • qiskit-aer v0.17 o posterior
  • scipy y matplotlib (para ajuste y graficación)
  • pylatexenc (para renderizar diagramas de circuitos)

Para configurar e instalar los paquetes anteriores, consulta la guía Instalar Qiskit. Para ejecutar trabajos en computadoras cuánticas reales, los estudiantes deben crear una cuenta con IBM Quantum® siguiendo los pasos de la guía Configura tu cuenta de IBM Cloud®.

Este módulo ha sido probado y utiliza aproximadamente 5 minutos de tiempo de QPU. Es una estimación y tu uso real puede variar.

Introducción​

Cuando un químico realiza un experimento de RMN, coloca una muestra de espines nucleares en un campo magnético intenso, aplica un pulso de radiofrecuencia (RF) y mide la señal que regresa. La técnica se basa en unas pocas ideas de la mecánica cuántica de una partícula de espín 1/2 en un campo magnético. El espín tiene dos niveles de energía separados por una energía que corresponde a una frecuencia de Larmor característica. Un pulso resonante a esa frecuencia lleva el espín al plano transversal, donde precesa e induce una señal que luego se desvanece con dos tiempos característicos, T1T_1 y T2T_2.

Un qubit superconductor es una versión diseñada del mismo objeto: un sistema cuántico controlable de dos niveles. Sus estados ∣0⟩|0\rangle y ∣1⟩|1\rangle son el análogo de espín hacia arriba y espín hacia abajo, la puerta que aplicamos para rotarlo es el análogo del pulso de RF, y pierde coherencia con sus propios T1T_1 y T2T_2. No se trata solo de una comparación vaga; algunos de los primeros procesadores cuánticos experimentales de finales de la década de 1990 y principios de la de 2000 usaron RMN.

En este módulo usarás una computadora cuántica real para ejecutar los mismos experimentos de caracterización que realiza un espectroscopista de RMN (o un ingeniero cuántico que calibra hardware):

  • Rabi / nutación — impulsar el espín y observar cómo rota. Así se calibra una rotación de π\pi o π/2\pi/2.

  • T1T_1 — relajación espín-red, el decaimiento de un espín excitado de vuelta al equilibrio.

  • T2T_2 — relajación espín-espín (desfase), medida con un experimento de Ramsey y reenfocada con un eco de Hahn.

  • Espectroscopia de RMN (desplazamiento químico y acoplamiento JJ) — simular una molécula pequeña en varios qubits y leer su espectro de multipletes para recuperar la conectividad mediante la regla n+1n+1.

Para cada concepto damos primero la teoría mínima y luego implementamos el experimento en Qiskit siguiendo el flujo de trabajo de cuatro pasos de los patrones de Qiskit.

Teoría​

Partículas de espín 1/2 en un campo magnético​

Un núcleo de espín 1/2 tiene un momento magnético μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}, donde γ\gamma es la razón giromagnética. En un campo estático B0B_0 a lo largo de zz, los dos estados de espín se separan en energía (el efecto 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 .

La brecha de energía define la frecuencia de Larmor (de resonancia) ω0=γB0\omega_0 = \gamma B_0. El momento magnético precesa alrededor de B0B_0 a esta misma frecuencia.

Para rotar el espín aplicamos un campo de RF oscilante en ω0\omega_0 (o cerca de ese valor). En el sistema de referencia que rota a la frecuencia de excitación (el sistema rotante), una excitación resonante rota el vector de Bloch alrededor de un eje en el plano xx-yy. Cuánto rota (el ángulo de giro) lo determinan la intensidad y la duración del pulso. Dos opciones son las más importantes: un pulso de 90∘90^\circ lleva un espín +1/2+1/2 al plano transversal, y un pulso de 180∘180^\circ lo rota por completo hasta −1/2-1/2.

Traducción entre la jerga de la RMN y la de la computación cuántica​

Un qubit es un sistema de dos niveles, matemáticamente idéntico a un espín 1/2 en un campo magnético — así que, aunque trabajamos en un procesador cuántico en lugar de un núcleo, la física es la misma que subyace a la RMN. Solo difiere el vocabulario. Esta tabla te ayuda a hablar tanto con químicos físicos como con especialistas en computación cuántica.

RMN / espín 1/2Procesador cuántico (qubit)
Espín hacia arriba / espín hacia abajo∣0⟩\ket{0} / ∣1⟩\ket{1}
Brecha de Zeeman ΔE=ℏω0\Delta E = \hbar\omega_0Energía de transición del qubit ℏω01\hbar\omega_{01}
Frecuencia de Larmor ω0\omega_0Frecuencia del qubit ω01\omega_{01}
Pulso de RF resonante (ángulo de giro θ\theta)Puerta de rotación de un solo qubit RX(θ)R_X(\theta)
Pulso de 90∘90^\circ / 180∘180^\circRX(π/2)R_X(\pi/2) / RX(π)R_X(\pi)
Relajación espín-red T1T_1Tiempo de relajación de energía del qubit T1T_1
Relajación espín-espín T2T_2Tiempo de desfase del qubit T2T_2
NutaciónOscilaciones de Rabi
Decaimiento de inducción libre (FID), eco de HahnSecuencias de Ramsey y de eco

Todo lo que sigue es un ejercicio de medir la columna de la derecha e interpretarla como la columna de la izquierda.

Configura el entorno de Qiskit​

Seguimos el flujo de trabajo de los patrones de Qiskit en todo momento:

  1. Mapear el problema a un circuito cuántico.

  2. Optimizar el circuito para su ejecución (transpilar al backend).

  3. Ejecutar usando una primitiva de IBM Quantum.

  4. Posprocesar y analizar.

Primero cargamos el servicio de runtime y elegimos el backend real menos ocupado. Si aún no has guardado tus credenciales, consulta la página de documentación sobre cómo guardar tus credenciales de inicio de sesión.

# 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 celda siguiente para alternar entre hardware real y un simulador local en todo el notebook. Ejecutar en hardware es el objetivo del módulo, pero si hay problemas de conectividad o de uso, el simulador puede servir de respaldo. Establece USE_HARDWARE = True para ejecutar en la QPU seleccionada arriba.

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

Experimento: oscilaciones de Rabi (nutación)​

Teoría​

Si impulsas un espín en resonancia, su vector de Bloch rota de forma constante de polo a polo — nuta. Si aplicamos una rotación de ángulo de giro θ\theta alrededor del eje xx a un qubit que parte de ∣0⟩|0\rangle, la probabilidad de encontrarlo en ∣1⟩|1\rangle es:

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

Esta es la curva de Rabi. Su primer máximo es el pulso π\pi (inversión completa, el pulso de 180∘180^\circ); un pulso π/2\pi/2 es un cuarto de vuelta (el pulso de 90∘90^\circ que lleva un espín al plano transversal). En RMN, esta curva es la forma de calibrar tus pulsos.

Salvedad experimental

Un experimento de Rabi literal barre la amplitud o la duración de la excitación en el tiempo. Eso requiere control a nivel de pulso, que se eliminó en Qiskit v2.0. Aquí, en cambio, barremos el ángulo de rotación θ\theta de una puerta RX(θ)R_X(\theta). La curva y la identificación de π\pi/π/2\pi/2 son idénticas; la única diferencia es que elegimos un ángulo directamente en lugar de calibrar la duración de un pulso. En hardware con puertas fraccionarias, RX(θ)R_X(\theta) para θ\theta arbitrario se ejecuta de forma nativa.

Una vista en la esfera de Bloch​

La siguiente animación muestra la nutación: una excitación resonante rota el vector de Bloch de forma constante alrededor del eje xx, llevándolo desde ∣0⟩|0\rangle hacia ∣1⟩|1\rangle y de vuelta. Este es exactamente el movimiento que mide la curva de Rabi.

Mapea el problema a un circuito​

Construimos un circuito parametrizado, RX(θ)R_X(\theta) seguido de una medición, y luego lo evaluamos en muchos ángulos a la vez.

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

Optimizar para la ejecución​

Transpila a un circuito de Arquitectura del Conjunto de Instrucciones (ISA) para el Backend elegido.

rabi_isa = pm_q.run(rabi)

Ejecutar​

Barremos θ\theta de 00 a 4π4\pi (dos ciclos completos de Rabi). Pasar un arreglo de valores de parámetros como un único Primitive Unified Bloc (PUB) los ejecuta todos en un solo trabajo.

angles = np.linspace(0, 4 * np.pi, 41)
job = sampler.run([(rabi_isa, angles.reshape(-1, 1))], shots=1000)
rabi_result = job.result()

Posprocesar y analizar​

Extrae P1P_1 en cada ángulo y compáralo 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

Comprueba tu comprensión​

En la curva que mediste, P1P_1 alcanza por primera vez su máximo en el pulso π\pi (θ=π\theta = \pi). Ahora supón que un dispositivo está mal calibrado, de modo que un ángulo nominal θ\theta en el eje x produce en realidad una rotación de 1.05 θ1.05\,\theta.

(a) ¿En qué ángulo nominal aparecería ahora el primer máximo de P1P_1? (b) ¿Sería la aplicación de un solo pulso π\pi una forma sensible de detectar este error del 5%?

Respuesta

(a) El máximo aparece donde la rotación real es igual a π\pi, es decir, en el ángulo nominal θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — un pequeño desplazamiento del pico hacia la izquierda.

(b) No. Un pulso π\pi nominal produce una rotación real de 1.05π1.05\pi, lo que da P1=sin⁡2(1.05π/2)≈0.994P_1 = \sin^2(1.05\pi/2) \approx 0.994 en lugar de 11 — un cambio demasiado pequeño para verse frente al ruido de medición. Por eso, para calibrar con precisión, los espectroscopistas de RMN amplifican el error aplicando la rotación muchas veces (por ejemplo, el método de 360∘360^\circ o un tren de pulsos largo), de modo que un pequeño error por pulso se acumula en una desviación grande y medible. Volvemos a esta idea en los problemas de desafío.

Experimento: T1T_1 (relajación espín-red)​

Teoría​

Si se lo deja solo, un espín excitado cede energía a su entorno (la "red") y se relaja de nuevo hacia el equilibrio térmico. La población del estado excitado decae exponencialmente con el tiempo de relajación espín-red T1T_1:

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

Para un químico, T1T_1 fija el retardo de reciclado que debes esperar entre barridos para obtener RMN cuantitativa, e informa sobre el giro molecular y la dinámica. En un qubit es el tiempo de relajación de energía, y es uno de los dos números principales que se usan para modelar los efectos de decoherencia en un ordenador cuántico.

El experimento. Prepara ∣1⟩|1\rangle con una puerta XX (un pulso π\pi), espera un retardo variable τ\tau y luego mide. La fracción que sigue en ∣1⟩|1\rangle traza el decaimiento.

Una vista en la esfera de Bloch​

La relajación T1T_1 devuelve el estado excitado al equilibrio, de modo que el vector de Bloch se desliza desde ∣1⟩|1\rangle de vuelta hacia ∣0⟩|0\rangle a lo largo del eje zz.

Construir y transpilar un barrido de retardos​

Las duraciones de los retardos deben ser valores concretos (se programan en tiempo de hardware), así que construimos un circuito por cada retardo.

# 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

Ejecutar​

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

Analizar: ajustar la exponencial​

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

El tiempo T1T_1 exacto varía de un experimento a otro y de un Backend a otro, pero deberías ver un resultado de alrededor de unos cientos de microsegundos.

Comprueba tu comprensión​

En la RMN cuantitativa se te dice que esperes unos 5 T15\,T_1 entre barridos. Usando la ley de decaimiento, ¿qué fracción de la magnetización de equilibrio se ha recuperado tras 5 T15\,T_1?

Respuesta

Tras 5 T15\,T_1, la desviación restante respecto al equilibrio es e−5≈0.0067e^{-5}\approx 0.0067, es decir, se ha recuperado aproximadamente un 99.3%.

Experimento: T2T_2, T2∗T_2^*​

Teoría​

T1T_1 mide la pérdida de energía. T2T_2 mide la pérdida de coherencia de fase en el plano transversal. Podemos observarlo con un experimento de Ramsey: un pulso π/2\pi/2 hace girar todos los espines hasta el ecuador, todos apuntando en la misma dirección. Precesan a la frecuencia de Larmor, pero cada uno precesa a una frecuencia ligeramente distinta, así que se abren en abanico a lo largo del ecuador y la señal transversal neta decae, con un tiempo de decaimiento característico de T2∗T_2^*.

Nota para los lectores cuánticos: La imagen del abanico muestra una forma de perder coherencia: una dispersión de fases definidas que se promedian entre sí. Conviene saber que este no es el único mecanismo. La coherencia también puede perderse porque la fase se entrelaza con el entorno, de modo que ya no es un valor definido que el sistema posea. No puedes distinguir los dos mecanismos solo observando el decaimiento de la señal, así que la imagen del abanico es una buena intuición. Ten cuidado de no interpretarla como la historia completa o literal.

Dos efectos desfasan un espín:

  • La decoherencia irreversible (o efectivamente irreversible) que surge de un acoplamiento no controlado con otros grados de libertad, caracterizada por el verdadero T2T_2. En RMN, esto proviene generalmente del movimiento molecular que modula los acoplamientos entre espines. En computación cuántica, corresponde a acoplamientos no deseados entre los qubits y el entorno.
  • Desplazamientos de frecuencia estáticos o de variación lenta, reversibles, como la inhomogeneidad del campo, la variación de frecuencia de un qubit a otro y los errores de calibración. Combinados con el verdadero T2T_2, producen el tiempo de decaimiento aparente más corto T2∗T_2^* que se observa en un experimento de Ramsey.

Entonces, ¿cómo recuperamos el verdadero T2T_2 si un experimento de Ramsey solo nos informa sobre T2∗T_2^*? El segundo experimento, llamado eco de Hahn, cancela todo el ruido del segundo tipo para hallar el verdadero T2T_2. Comienza igual que un experimento de Ramsey, con un pulso π/2\pi/2, y luego los espines se abren en abanico como antes. Tras un tiempo τ\tau, se aplica un pulso π\pi. El pulso invierte la acumulación de fase causada por los desplazamientos de frecuencia estáticos, de modo que los espines se reenfocan tras un segundo retardo de la misma duración, τ\tau.

T2∗T_2^* y T2T_2 son números importantes que conviene conocer tanto en el contexto de la química como en el de la computación cuántica. En química, T2∗T_2^* fija el ancho de línea de un experimento de RMN estándar (verás lo que esto significa en una sección posterior). T2T_2 puede decirnos mucho sobre las moléculas, como el giro molecular, y un cambio en T2T_2 puede indicar que una molécula se ha unido a algo más grande. En computación cuántica, generalmente queremos que tanto T2T_2 como T2∗T_2^* sean lo más largos posible, para tener más tiempo de realizar experimentos antes de que la decoherencia domine el sistema. Toda una familia de técnicas basadas en el eco de Hahn extiende este tiempo cancelando el ruido reversible, de modo que el circuito queda idealmente limitado por el verdadero T2T_2, no por T2∗T_2^*. Esto se llama "desacoplamiento dinámico".

Implementación​

Cuando implementamos los experimentos de Ramsey y de eco de Hahn en un qubit, la lectura es ligeramente distinta que en el caso de la RMN. En RMN, puedes observar directamente los campos oscilantes inducidos por los espines que precesan a la frecuencia de Larmor en el plano transversal. Pero con un qubit solo podemos medir su proyección a lo largo del eje ZZ, es decir, si es ∣0⟩\vert 0 \rangle o ∣1⟩\vert 1 \rangle. Por tanto, en ambos experimentos terminamos con un pulso π/2\pi/2 adicional seguido de una medición. Este pulso π/2\pi/2 traduce la información de fase del qubit en una población.

A continuación ejecutamos cada experimento para determinar T2∗T_2^* y T2T_2.

Ramsey (T2∗T_2^{*}): dos pulsos π/2\pi/2 con tiempo de retardo τ\tau​

El primer pulso π/2\pi/2 lleva el vector de Bloch al ecuador, y el siguiente traduce la fase del vector de Bloch en una población. Como la fase cambia con el tiempo a medida que el vector de Bloch precesa, vemos un patrón sinusoidal en la población en función del tiempo de precesión τ\tau. A medida que el qubit pierde coherencia de fase, deberíamos ver decaer la amplitud de esta curva sinusoidal.

Algunas sutilezas más para los lectores cuánticos: En los experimentos de computación cuántica, las oscilaciones que vemos no están realmente a la frecuencia de Larmor, que está en el rango de los GHz y es demasiado rápida para que podamos resolverla. Los experimentos cuánticos operan en el sistema de referencia rotante: un sistema que gira junto con el qubit a (casi) la frecuencia de Larmor, de modo que la precesión rápida es invisible y el vector de Bloch parece casi inmóvil. Para ver franjas, fijamos deliberadamente nuestro sistema rotante ligeramente desintonizado respecto a la frecuencia de Larmor. El vector de Bloch parece entonces precesar a esa pequeña desintonía, y obtenemos una señal oscilante a la frecuencia de desintonía. A continuación, no fijamos la desintonía en un único valor; la elegimos de modo que unas doce franjas abarquen la ventana de medición (que dimensionamos según el tiempo de coherencia del qubit), lo que mantiene la oscilación fácil de muestrear sin aliasing. Una complicación más: en Qiskit no tenemos el control a nivel de pulso necesario para cambiar la frecuencia del pulso (que es lo que fija la frecuencia del sistema rotante). En su lugar, añadimos a mano una desintonía efectiva, usándola para fijar la fase del segundo pulso de Ramsey. Es algo artificioso, pero el resultado es el mismo y el decaimiento que observamos sigue siendo el genuino T2∗T_2^*.

Una vista en la esfera de Bloch​

Un pulso π/2\pi/2 lleva los espines al ecuador; cada uno precesa entonces a una velocidad ligeramente distinta y se abren en abanico, de modo que el vector neto (rojo) se encoge a medida que se desfasan: este decaimiento es 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 de Hahn (T2T_2): añadir un pulso π\pi de reenfoque en el medio​

Una vista en la esfera de Bloch​

El eco de Hahn: los espines se abren en abanico como antes, luego un pulso π\pi los invierte, de modo que su precesión continuada los reenfoca de nuevo juntos (el vector rojo vuelve a crecer), recuperando la coherencia perdida por el desfase estático.

# 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

Analizar: ajustar y comparar los dos decaimientos​

# 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

Aquí vemos tanto la oscilación de Ramsey como el decaimiento de la señal del eco de Hahn, como era de esperar. Las escalas de tiempo asociadas a cada uno de estos decaimientos son T2∗T_2^* y T2T_2, respectivamente.

Comprueba tu comprensión​

Normalmente deberías encontrar T2≳T2∗T_2 \gtrsim T_2^{*}. Explica por qué en una frase, e indica qué efecto físico elimina el pulso π\pi del eco de Hahn, así como qué efecto no puede eliminar.

Respuesta

T2∗T_2^{*} incluye tanto el desfase irreversible como la dispersión estática reversible, por lo que decae más rápido; el pulso π\pi del eco reenfoca el desfase estático y reversible (inhomogeneidad del campo / desplazamientos de frecuencia fijos), dejando solo el desfase espín-espín irreversible, que es el verdadero T2T_2. El eco no puede deshacer las fluctuaciones que cambian entre las dos mitades de la secuencia.

RMN de una molécula: desplazamiento químico y acoplamiento JJ​

De un espín a una molécula​

Los experimentos hasta ahora caracterizaron un único espín aislado. La RMN química real lee una molécula (muchos espines acoplados a la vez) y usa el espectro resultante para inferir características de la estructura de la molécula. Esto puede informarnos sobre dos efectos:

  • Desplazamiento químico. Cada núcleo químicamente distinto precesa a una frecuencia νi\nu_i ligeramente diferente, porque sus electrones locales apantallan el campo aplicado en distinta medida. Por tanto, las posiciones de los picos identifican los entornos químicos (un protón de CH3_3 frente a uno aromático, por ejemplo).

  • Acoplamiento escalar JJ. Una interacción a través de los enlaces entre dos espines divide cada pico en un multiplete. Un núcleo con nn vecinos equivalentes se divide en n+1n+1 líneas con intensidades binomiales, la regla n+1n+1. El patrón de división revela la conectividad (qué núcleos están enlazados), y el espaciado es la constante de acoplamiento JJ.

El experimento​

Para modelar esto en nuestro ordenador cuántico, necesitamos implementar un hamiltoniano de espín sobre los qubits. En el límite de acoplamiento débil y campo alto (diferencias de desplazamiento químico ≫\gg acoplamientos), las partes XXXX y YYYY del acoplamiento se promedian a cero, dejando solo ZZZZ. Así, el hamiltoniano es (unidades de frecuencia angular, ℏ=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}}.

Cada núcleo es un qubit: el desplazamiento químico es una rotación ZZ de un solo qubit, y el acoplamiento es una interacción ZZZZ de dos qubits.

El experimento procede de la siguiente manera. Un pulso π/2\pi/2 lleva cada espín al plano transversal, y los espines precesan y se acoplan durante un tiempo tt. Registramos la magnetización transversal compleja s(t)=⟨X⟩+i⟨Y⟩s(t) = \langle X\rangle + i\langle Y\rangle sumada sobre todos los espines. Como en el experimento de Ramsey anterior, esta magnetización transversal traza una sinusoide decreciente en función del tiempo de precesión tt. Cuando tomamos la transformada de Fourier de la señal, obtenemos un espectro con picos en el desplazamiento químico νi\nu_i divididos por el acoplamiento escalar JijJ_{ij}.

nota

Simulamos la evolución de la molécula asignando el tiempo tt a ángulos de rotación: un tt mayor significa rotaciones ZZ y ZZZZ mayores, no un circuito más largo o más profundo. Así, aunque el estado simulado sí avanza con tt (eso es lo que hace oscilar el decaimiento de inducción libre (FID)), los qubits físicos nunca permanecen inactivos más tiempo y no hay decoherencia dependiente de tt. Por tanto, el FID simulado nunca decae por sí solo. Para un FID ideal infinitamente largo, las líneas espectrales serían infinitamente nítidas; nuestra ventana de muestreo finita les da, en cambio, una resolución finita y puede introducir fuga espectral. Las líneas reales de RMN tienen un ancho finito determinado por T2∗T_2^*. Lo reincorporamos multiplicando el FID por una ventana e−t/T2∗e^{-t/T_2^{*}} antes de la transformada de Fourier. Esto se llama apodización de ensanchamiento de línea, que los espectroscopistas aplican durante el procesamiento para fijar el ancho de línea (intercambiando resolución por relación señal-ruido). (Una celda opcional en el Apéndice de este módulo muestra una alternativa trotterizada, donde el decaimiento procede en cambio de una decoherencia real que crece con la profundidad del circuito.)

El sistema AX​

Empecemos con el llamado sistema AX, una molécula formada por dos espines acoplados y químicamente distintos. Las etiquetas A y X denotan los dos entornos de espín químicamente distintos. Las letras son arbitrarias, pero el hecho de que estén tan separadas en el alfabeto ilustra su disimilitud química.

Mapear y optimizar​

Escribimos una función que, dados los desplazamientos químicos νi\nu_i y los acoplamientos JijJ_{ij}, devuelve un circuito de preparación de estado parametrizado. El pulso π/2\pi/2 lleva los espines al ecuador, y luego las rotaciones ZZ simulan la evolución del estado bajo los términos de desplazamiento químico y de acoplamiento JJ. Cada νi\nu_i es un pequeño desplazamiento de precesión en Hz relativo a una referencia elegida, o portadora del sistema rotante. Aquí lo fijamos manualmente; en una molécula real, el apantallamiento electrónico cambia el campo magnético local y desplaza la resonancia de cada núcleo respecto a esa referencia. Esto se modela con las puertas RZR_Z de un solo qubit, una por espín, y cada νi\nu_i fija dónde se sitúa el pico de ese espín en el espectro. La puerta ZZZZ de dos qubits aplica el acoplamiento JijJ_{ij}, que divide cada pico en un multiplete y fija el espaciado entre sus líneas.

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

Ejecutar y posprocesar​

Evaluamos el circuito sobre una malla de tiempos de evolución, y el Estimator devuelve ⟨X⟩\langle X\rangle y ⟨Y⟩\langle Y\rangle en cada uno. Los combinamos en el FID complejo ⟨X⟩+i⟨Y⟩\langle X\rangle + i\langle Y\rangle, aplicamos la ventana de apodización y aplicamos la transformada de Fourier al resultado para obtener el espectro.

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

Extensión: un sistema AX2_2 y la regla n+1n+1​

Ahora añade un segundo vecino equivalente. En un sistema AX2_2, un espín A se acopla, con el mismo JJ, a dos espines X equivalentes. (Recuerda que X es solo una etiqueta para los dos espines equivalentes; no tiene nada que ver con los operadores de Pauli XX. Todos los acoplamientos siguientes siguen siendo ZZZZ.) El espín A ve entonces cuatro configuraciones de vecinos (↑↑, ↑↓, ↓↑, ↓↓)(\uparrow\uparrow,\ \uparrow\downarrow,\ \downarrow\uparrow,\ \downarrow\downarrow); las dos mixtas coinciden, así que A se divide en un triplete 1:2:1 (n=2⇒n+1=3n=2 \Rightarrow n+1 = 3 líneas). Cada espín X ve solo a A y sigue siendo un doblete. Los espines equivalentes no se dividen entre sí, por lo que no incluimos acoplamiento X–X.

Una molécula real con este patrón es el 1,1,2-tricloroetano (Cl2_2CH–CH2_2Cl). Cuando nos centramos en los espines del hidrógeno (que es la forma más común de RMN), el único protón de hidrógeno en el CH hace el papel de A, y los dos protones equivalentes del CH2_2 son los dos espines X. Ahora basta con contar vecinos: el protón del CH tiene dos vecinos (el par CH2_2), así que su señal se divide en un triplete 1:2:1 (n=2n=2); cada protón del CH2_2 tiene solo un vecino (el CH), así que la señal del CH2_2 es un doblete 1:1 (n=1n=1). Observa que el grupo con menos protones (CH) muestra más líneas; el número de líneas refleja cuántos vecinos tiene una señal, no cuántos protones la comparten.

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

A continuación, simulamos un sistema AX2_2 con tres qubits.

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

Comprueba tu comprensión​

Lee cada pregunta, piensa tu respuesta y luego haz clic en el triángulo para revelar la solución.

En el espectro AX, los dos dobletes están separados (de centro a centro) por 12 Hz, y las dos líneas dentro de cada doblete están a 2 Hz de distancia. ¿Cuál número es la diferencia de desplazamiento químico y cuál es la constante de acoplamiento JJ? Si pasaras a un espectrómetro con el doble de campo magnético, ¿cuál cambiaría?

Respuesta

La separación de 12 Hz de centro a centro es la diferencia de desplazamiento químico (ν1−ν2\nu_1-\nu_2); el espaciado de líneas de 2 Hz es la constante de acoplamiento JJ. Los desplazamientos químicos provienen del apantallamiento de B0B_0, por lo que escalan con el campo. Con el doble de campo, la separación se duplica a 24 Hz. El acoplamiento JJ es una propiedad molecular a través de los enlaces y es independiente del campo, así que se mantiene en 2 Hz. Los químicos distinguen el desplazamiento del acoplamiento observando cómo escala una división con el campo.

Usando la regla n+1n+1, predice el multiplete de A si se añadiera un tercer espín X equivalente (un sistema AX3_3), y explica por qué los dos espines X equivalentes en AX2_2 no se dividen entre sí.

Respuesta

Con n=3n=3 vecinos equivalentes, A se convierte en un cuarteto 1:3:3:1. Los espines equivalentes no se dividen entre sí porque su acoplamiento desplaza todos los niveles de energía relevantes en la misma cantidad, de modo que las diferencias entre esos niveles — las frecuencias que vemos — no cambian.

Conclusión​

Hemos usado un ordenador cuántico real para explorar cómo los espines nucleares precesan y se decohieren en un experimento de RMN, y cómo interpretar los espectros resultantes. Vale la pena dar un paso atrás para repasar por qué esto fue posible y por qué es más que una curiosidad.

Un qubit y un núcleo de espín 1/2 son, matemáticamente, el mismo objeto: sistemas cuánticos de dos niveles que viven en la esfera de Bloch, rotados por pulsos resonantes, que precesan alrededor de un eje y pierden coherencia con su entorno. Todo el vocabulario de este módulo (la frecuencia de Larmor, el sistema rotante, T1T_1, T2T_2 y T2∗T_2^*, las franjas de Ramsey, el eco de Hahn) pertenece por igual a la RMN y a la computación cuántica. Lo que un químico llama desplazamiento químico, un ingeniero cuántico lo llama rotación ZZ de un solo qubit; lo que un químico llama acoplamiento JJ, un ingeniero cuántico lo llama interacción ZZZZ. Los dos campos han estado describiendo la misma física con palabras distintas.

Esa conexión es histórica, no solo conceptual: la RMN fue una de las primeras plataformas en ejecutar algoritmos cuánticos reales. En 2001, una molécula en solución había ejecutado el algoritmo de Shor para factorizar 15, con cada espín nuclear actuando como un qubit. La RMN también legó a la computación cuántica gran parte de su caja de herramientas de control. El eco de espín, los trenes de pulsos CPMG, los pulsos compuestos y el desacoplamiento dinámico fueron todos inventados por espectroscopistas décadas antes de que los qubits los necesitaran para combatir la decoherencia. Hay una simetría satisfactoria en lo que acabamos de hacer: el campo que dio origen a los primeros ordenadores cuánticos es ahora algo que un ordenador cuántico puede simular.

Repaso y preguntas​

Conceptos críticos​

  • Un qubit es un sistema de dos niveles diseñado, equivalente a un espín 1/2; su ∣0⟩/∣1⟩|0\rangle/|1\rangle, frecuencia, puertas, T1T_1 y T2T_2 son los análogos directos de las magnitudes de la RMN.

  • Un pulso resonante hace rotar el vector de Bloch; el ángulo de giro fija la rotación, y la curva de Rabi/nutación P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) identifica las rotaciones π\pi y π/2\pi/2.

  • T1T_1 es la relajación de energía (espín-red); T2T_2 es la relajación de fase (espín-espín); T2∗T_2^{*} incluye además el desfase estático reversible que un eco de Hahn reenfoca.

  • Una molécula es un conjunto de espines acoplados: el desplazamiento químico fija la posición de cada pico, y el acoplamiento JJ divide los picos en multipletes siguiendo la regla n+1n+1, lo que en conjunto codifica el entorno químico y la conectividad.

Verdadero / falso​

  1. V/F La frecuencia de Larmor es la diferencia de energía entre los dos estados de espín dividida por ℏ\hbar.

  2. V/F Un pulso de 90∘90^\circ (π/2\pi/2) invierte por completo un espín en equilibrio.

  3. V/F T2∗T_2^{*} es generalmente más largo que T2T_2.

  4. V/F El eco de Hahn reenfoca el desfase espín-espín irreversible.

  5. V/F En un sistema AX2_2 el núcleo A aparece como un triplete 1:2:1.

  6. V/F La constante de acoplamiento JJ se duplica cuando duplicas el campo magnético del espectrómetro.

  7. V/F Eliminar Qiskit Pulse hace imposible medir T1T_1 en el hardware actual.

Respuesta corta​

  1. Explica en dos frases por qué un solo pulso π\pi es una mala forma de detectar un error del 5%5\% en el ángulo de giro, y cómo lo soluciona encadenar pulsos.

  2. Una muestra presenta líneas de RMN anchas. En términos de T2T_2, ¿qué te indica eso, y cuál es la magnitud análoga en un qubit?

  3. Da el análogo en el qubit de cada uno: (a) el retardo de reciclado, (b) el pulso de 90∘90^\circ, (c) el desplazamiento químico.

  4. Usando la regla n+1n+1, explica por qué un grupo etilo (CH3_3CH2_2–) muestra un triplete 1:2:1 y un cuarteto 1:3:3:1, e identifica qué grupo produce cada multiplete.

Problemas de desafío​

  1. Tren de Carr-Purcell. Modifica el eco de Hahn para insertar nn pulsos π\pi igualmente espaciados (un tren de estilo CPMG) y mide T2T_2 en función de nn. Discute por qué más pulsos de reenfoque pueden alargar el T2T_2 aparente.

  2. Amplificación de errores de pulso. Construye un circuito que aplique RX(π)R_X(\pi)  n\,n veces a ∣0⟩|0\rangle y representa P1P_1 frente a nn para un ángulo deliberadamente mal ajustado RX(1.05π)R_X(1.05\pi). Muestra cómo el error se hace visible a medida que nn crece. Pista: coloca una barrera después de cada rotación; de lo contrario, el transpilador puede combinar las rotaciones repetidas en una única puerta equivalente.

  3. Grupo etilo. Usando fid_circuit, construye el espectro de protones de un fragmento etilo (CH3_3–CH2_2): cinco qubits, con los tres protones de CH3_3 compartiendo un desplazamiento químico y los dos protones de CH2_2 otro, y cada par CH3_3–CH2_2 acoplado con el mismo JJ (sin acoplamiento dentro de un grupo equivalente). Predice el patrón con la regla n+1n+1 y luego simúlalo; deberías recuperar un cuarteto 1:3:3:1 (CH2_2) y un triplete 1:2:1 (CH3_3).

Apéndice (opcional): evolución trotterizada: dejar que la decoherencia fije el ancho de línea​

En la sección de RMN de una molécula, la evolución e−iHte^{-iHt} se aplica en una única capa cuya profundidad no crece con tt, por lo que el FID no decae por sí solo e imponemos el ancho de línea con la ventana de apodización e−t/T2e^{-t/T_2}. Esta celda opcional adopta el enfoque opuesto. Divide la evolución en pp pasos de Trotter y coloca una barrera después de cada paso (para que el transpilador no pueda volver a fusionarlos en una sola capa). El punto temporal kk usa kk pasos, de modo que la profundidad del circuito crece con el tiempo.

Como todos los términos de HH conmutan, esto es exacto: en un simulador sin ruido da los mismos picos nítidos. Pero en un Backend con modelo de ruido o en hardware real el error de puerta acumulado hace que el FID decaiga por sí solo, y ese decaimiento es lo que fija el ancho de línea. Esta es la idea T2↔T_2\leftrightarrowancho de línea de la sección del experimento de T2T_2, T2∗T_2^*, que ahora surge de una decoherencia real en lugar de una ventana de procesamiento. Ejecutamos de esta manera tanto el sistema AX (dos dobletes) como el sistema AX2_2 (un triplete 1:2:1 más un doblete).

Dos notas prácticas a considerar. Primero, el coste es la profundidad: el circuito AX más profundo usa ∼ ⁣47\sim\!47 puertas de dos qubits y la versión AX2_2 aproximadamente el doble, por lo que ambos son mucho más ruidosos y costosos que la versión exacta. En segundo lugar, cada sistema de espines se sigue enviando como un solo trabajo, y cada punto temporal reutiliza la misma disposición de qubits físicos seleccionada en la sección de espectroscopía. Si USE_BATCH=True, estos trabajos también usan el Batch opcional de todo el 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()