Pular para o conteúdo principal
Este projeto está em fase beta. Encontrou um bug? Abra uma issue no GitHub. Tem uma pergunta ou ideia? Pergunte no Discussions — adoraríamos receber seu feedback!
Como está a qualidade da tradução nesta página?

Ressonância magnética nuclear com Qiskit

Para este módulo do Qiskit in Classrooms, os alunos precisam ter um ambiente Python funcional com os seguintes pacotes instalados:

  • qiskit v2.1 ou mais recente
  • qiskit-ibm-runtime v0.40.1 ou mais recente
  • qiskit-aer v0.17 ou mais recente
  • scipy e matplotlib (para ajuste e plotagem)
  • pylatexenc (para renderizar diagramas de circuitos)

Para configurar e instalar os pacotes acima, consulte o guia Instalar o Qiskit. Para executar jobs em computadores quânticos reais, os alunos devem criar uma conta na IBM Quantum® seguindo as etapas do guia Configure sua conta da IBM Cloud®.

Este módulo foi testado e usa aproximadamente 5 minutos de tempo de QPU. Esta é uma estimativa, e seu uso real pode variar.

Introdução​

Quando um químico executa um experimento de RMN, ele coloca uma amostra de spins nucleares em um campo magnético forte, aplica um pulso de radiofrequência (RF) e mede o sinal que retorna. A técnica se baseia em algumas ideias da mecânica quântica de uma partícula de spin 1/2 em um campo magnético. O spin tem dois níveis de energia separados por uma energia que corresponde a uma frequência de Larmor característica. Um pulso ressonante nessa frequência inclina o spin para o plano transversal, onde ele precessiona e induz um sinal que então decai com dois tempos característicos, T1T_1 e T2T_2.

Um qubit supercondutor é uma versão projetada do mesmo objeto: um sistema quântico controlável de dois níveis. Seus estados ∣0⟩|0\rangle e ∣1⟩|1\rangle são o análogo de spin para cima e spin para baixo, a porta que aplicamos para girá-lo é o análogo do pulso de RF, e ele perde coerência com seus próprios T1T_1 e T2T_2. Isso não é apenas uma comparação vaga; alguns dos primeiros processadores quânticos experimentais, no fim da década de 1990 e início dos anos 2000, usavam RMN.

Neste módulo, você usará um computador quântico real para executar os mesmos experimentos de caracterização que um espectroscopista de RMN (ou um engenheiro quântico calibrando hardware) realiza:

  • Rabi / nutação — acionar o spin e observá-lo girar. É assim que você calibra uma rotação de π\pi ou π/2\pi/2.

  • T1T_1 — relaxação spin-rede, o decaimento de um spin excitado de volta ao equilíbrio.

  • T2T_2 — relaxação spin-spin (defasagem), medida com um experimento de Ramsey e refocalizada com um eco de Hahn.

  • Espectroscopia de RMN (deslocamento químico e acoplamento JJ) — simular uma molécula pequena em vários qubits e ler seu espectro de multipletos para recuperar a conectividade por meio da regra n+1n+1.

Para cada conceito, primeiro apresentamos a teoria mínima e depois implementamos o experimento no Qiskit seguindo o fluxo de trabalho de quatro etapas dos Qiskit patterns.

Teoria​

Partículas de spin 1/2 em um campo magnético​

Um núcleo de spin 1/2 tem um momento magnético μ=γℏI\boldsymbol{\mu} = \gamma \hbar \mathbf{I}, em que γ\gamma é a razão giromagnética. Em um campo estático B0B_0 ao longo de zz, os dois estados de spin se separam em energia (o efeito 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 .

A diferença de energia define a frequência de Larmor (de ressonância) ω0=γB0\omega_0 = \gamma B_0. O momento magnético precessiona em torno de B0B_0 nessa mesma frequência.

Para girar o spin, aplicamos um campo de RF oscilante em (ou perto de) ω0\omega_0. No referencial que gira na frequência de acionamento (o referencial girante), um acionamento ressonante gira o vetor de Bloch em torno de um eixo no plano xx-yy. O quanto ele gira (o ângulo de rotação) é determinado pela intensidade e pela duração do pulso. Duas escolhas importam mais: um pulso de 90∘90^\circ inclina um spin +1/2+1/2 para o plano transversal, e um pulso de 180∘180^\circ o gira completamente para −1/2-1/2.

Traduzindo entre o jargão de RMN e o da computação quântica​

Um qubit é um sistema de dois níveis, matematicamente idêntico a um spin 1/2 em um campo magnético — portanto, embora estejamos executando em um processador quântico e não em um núcleo, a física é a mesma que está por trás da RMN. Só o vocabulário muda. Esta tabela ajuda você a conversar tanto com físico-químicos quanto com especialistas em computação quântica.

RMN / spin 1/2Processador quântico (qubit)
Spin para cima / spin para baixo∣0⟩\ket{0} / ∣1⟩\ket{1}
Gap de Zeeman ΔE=ℏω0\Delta E = \hbar\omega_0Energia de transição do qubit ℏω01\hbar\omega_{01}
Frequência de Larmor ω0\omega_0Frequência do qubit ω01\omega_{01}
Pulso de RF ressonante (ângulo de rotação θ\theta)Porta de rotação de um único qubit RX(θ)R_X(\theta)
Pulso de 90∘90^\circ / 180∘180^\circRX(π/2)R_X(\pi/2) / RX(π)R_X(\pi)
Relaxação spin-rede T1T_1Tempo de relaxação de energia do qubit T1T_1
Relaxação spin-spin T2T_2Tempo de defasagem do qubit T2T_2
NutaçãoOscilações de Rabi
Decaimento de indução livre (FID), eco de HahnSequências de Ramsey e de eco

Tudo o que vem a seguir é um exercício de medir a coluna da direita e interpretá-la como a coluna da esquerda.

Configure o ambiente do Qiskit​

Seguimos o fluxo de trabalho dos Qiskit patterns do início ao fim:

  1. Mapear o problema para um circuito quântico.

  2. Otimizar o circuito para execução (transpilar para o backend).

  3. Executar usando uma primitiva do IBM Quantum.

  4. Pós-processar e analisar.

Primeiro, carregamos o serviço de runtime e escolhemos o backend real menos ocupado. Se você ainda não salvou suas credenciais, consulte a página da documentação sobre como salvar suas credenciais de login.

# 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

Use a célula abaixo para alternar entre hardware real e um simulador local em todo o notebook. Executar em hardware é o objetivo do módulo, mas, se houver problemas de conectividade ou de uso, o simulador pode servir de alternativa. Defina USE_HARDWARE = True para executar na QPU selecionada acima.

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: oscilações de Rabi (nutação)​

Teoria​

Acione um spin na ressonância e seu vetor de Bloch gira de forma constante de um polo ao outro — ele nuta. Se aplicarmos uma rotação de ângulo θ\theta em torno do eixo xx a um qubit que começa em ∣0⟩|0\rangle, a probabilidade de encontrá-lo em ∣1⟩|1\rangle é:

P1(θ)=sin⁡2 ⁣(θ2)=12(1−cos⁡θ).P_1(\theta) = \sin^2\!\left(\tfrac{\theta}{2}\right) = \tfrac{1}{2}\bigl(1 - \cos\theta\bigr).

Esta é a curva de Rabi. Seu primeiro máximo é o pulso π\pi (inversão completa, o pulso de 180∘180^\circ); um pulso π/2\pi/2 é um quarto de volta (o pulso de 90∘90^\circ que inclina um spin para o plano transversal). Na RMN, essa curva é como você calibra seus pulsos.

Ressalva experimental

Um experimento de Rabi literal varre a amplitude ou a duração do acionamento no tempo. Isso exige controle em nível de pulso, que foi removido no Qiskit v2.0. Aqui, em vez disso, varremos o ângulo de rotação θ\theta de uma porta RX(θ)R_X(\theta). A curva e a identificação de π\pi/π/2\pi/2 são idênticas; a única diferença é que escolhemos um ângulo diretamente, em vez de calibrar a duração de um pulso. Em hardware com portas fracionárias, RX(θ)R_X(\theta) para θ\theta arbitrário é executada nativamente.

Uma visão na esfera de Bloch​

A animação abaixo mostra a nutação: um acionamento ressonante gira o vetor de Bloch de forma constante em torno do eixo xx, levando-o de ∣0⟩|0\rangle em direção a ∣1⟩|1\rangle e de volta. Este é exatamente o movimento que a curva de Rabi mede.

Mapeie o problema para um circuito​

Construímos um circuito parametrizado, RX(θ)R_X(\theta) seguido de uma medição, e depois o avaliamos em muitos ângulos de uma só 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

Otimizar para execução​

Transpile para um circuito de Instruction Set Architecture (ISA) para o backend escolhido.

rabi_isa = pm_q.run(rabi)

Executar​

Variamos θ\theta de 00 a 4π4\pi (dois ciclos de Rabi completos). Passar um array de valores de parâmetros como um único Primitive Unified Bloc (PUB) executa todos eles em um só job.

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

Pós-processar e analisar​

Extraia P1P_1 em cada ângulo e compare com 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

Verifique seu entendimento​

Na curva que você mediu, P1P_1 atinge seu máximo pela primeira vez no pulso π\pi (θ=π\theta = \pi). Agora suponha que um dispositivo esteja descalibrado, de modo que um ângulo nominal θ\theta no eixo x produza, na verdade, uma rotação de 1.05 θ1.05\,\theta.

(a) Em que ângulo nominal o primeiro máximo de P1P_1 apareceria agora? (b) Aplicar um único pulso π\pi seria uma maneira sensível de detectar esse erro de 5%?

Resposta

(a) O máximo aparece onde a rotação real é igual a π\pi, ou seja, no ângulo nominal θ=π/1.05≈0.95π\theta = \pi/1.05 \approx 0.95\pi — um pequeno deslocamento do pico para a esquerda.

(b) Não. Um pulso π\pi nominal produz uma rotação real de 1.05π1.05\pi, resultando em P1=sin⁡2(1.05π/2)≈0.994P_1 = \sin^2(1.05\pi/2) \approx 0.994 em vez de 11 — uma mudança pequena demais para ser vista em meio ao ruído de medição. É por isso que, para calibrar com precisão, os espectroscopistas de RMN amplificam o erro aplicando a rotação muitas vezes (por exemplo, o método de 360∘360^\circ ou um trem de pulsos longo), de modo que um pequeno erro por pulso se acumule em um desvio grande e mensurável. Voltamos a essa ideia nos problemas desafio.

Experimento: T1T_1 (relaxação spin-rede)​

Teoria​

Se deixado sozinho, um spin excitado cede energia ao ambiente (a "rede") e relaxa de volta em direção ao equilíbrio térmico. A população do estado excitado decai exponencialmente com o tempo de relaxação spin-rede T1T_1:

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

Para um químico, T1T_1 define o atraso de reciclagem que você deve esperar entre varreduras em RMN quantitativa, e informa sobre o movimento molecular de rotação e a dinâmica. Em um qubit, é o tempo de relaxação de energia e é um dos dois principais números usados para modelar os efeitos de decoerência em um computador quântico.

O experimento. Prepare ∣1⟩|1\rangle com uma porta XX (um pulso π\pi), espere um atraso variável τ\tau e então meça. A fração que ainda está em ∣1⟩|1\rangle traça o decaimento.

Uma visão na esfera de Bloch​

A relaxação T1T_1 leva o estado excitado de volta ao equilíbrio, de modo que o vetor de Bloch desliza de ∣1⟩|1\rangle de volta para ∣0⟩|0\rangle ao longo do eixo zz.

Construir e transpilar uma varredura de atrasos​

As durações dos atrasos precisam ser valores concretos (elas são agendadas no tempo do hardware), então construímos um circuito para cada atraso.

# 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

Executar​

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

Analisar — ajustar a 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

O tempo T1T_1 exato varia de experimento para experimento e de backend para backend, mas você deve ver um resultado em torno de algumas centenas de microssegundos.

Verifique seu entendimento​

Em RMN quantitativa, você é instruído a esperar cerca de 5 T15\,T_1 entre varreduras. Usando a lei de decaimento, que fração da magnetização de equilíbrio se recuperou após 5 T15\,T_1?

Resposta

Após 5 T15\,T_1, o desvio restante do equilíbrio é e−5≈0.0067e^{-5}\approx 0.0067, ou seja, cerca de 99,3% recuperado.

Experimento: T2T_2, T2∗T_2^*​

Teoria​

T1T_1 mede a perda de energia. T2T_2 mede a perda de coerência de fase no plano transversal. Podemos observar isso com um experimento de Ramsey: um pulso π/2\pi/2 gira todos os spins para baixo até o equador, todos apontando na mesma direção. Eles precessam à frequência de Larmor, mas cada um precessa a uma frequência ligeiramente diferente, então se espalham em leque ao longo do equador e o sinal transversal líquido decai, com um tempo de decaimento característico de T2∗T_2^*.

Observação para leitores de computação quântica: A imagem do espalhamento em leque mostra uma maneira de perder coerência: uma dispersão de fases definidas que se anulam na média. Vale saber que esse não é o único mecanismo. A coerência também pode ser perdida porque a fase se emaranha com o ambiente, de modo que deixa de ser um valor definido que o sistema possui. Você não consegue distinguir os dois mecanismos apenas observando o decaimento do sinal, então a imagem do espalhamento em leque é uma boa intuição. Tome cuidado para não lê-la como a história completa ou literal.

Dois efeitos defasam um spin:

  • Decoerência irreversível (ou efetivamente irreversível) decorrente do acoplamento não controlado com outros graus de liberdade, caracterizada pelo T2T_2 verdadeiro. Em RMN, isso geralmente vem do movimento molecular modulando os acoplamentos entre spins. Em computação quântica, corresponde a acoplamentos indesejados entre qubits e o ambiente.
  • Deslocamentos de frequência estáticos ou de variação lenta, reversíveis, como a inomogeneidade do campo, a variação de frequência entre qubits e erros de calibração. Combinados com o T2T_2 verdadeiro, eles produzem o tempo de decaimento aparente mais curto T2∗T_2^* observado em um experimento de Ramsey.

Então, como recuperamos o T2T_2 verdadeiro se um experimento de Ramsey só nos informa sobre T2∗T_2^*? O segundo experimento, chamado de eco de Hahn, cancela todo o ruído do segundo tipo para encontrar o T2T_2 verdadeiro. Ele começa igual a um experimento de Ramsey, com um pulso π/2\pi/2, e então os spins se espalham em leque como antes. Após algum tempo τ\tau, um pulso π\pi é aplicado. O pulso inverte o acúmulo de fase causado por deslocamentos de frequência estáticos, de modo que os spins se refocalizam após um segundo atraso de mesma duração, τ\tau.

T2∗T_2^* e T2T_2 são números importantes de conhecer tanto no contexto da química quanto no da computação quântica. No contexto da química, T2∗T_2^* define a largura de linha de um experimento de RMN padrão (você verá o que isso significa em uma seção posterior). T2T_2 pode dizer muito sobre as moléculas, como o movimento molecular de rotação, e uma mudança em T2T_2 pode sinalizar que uma molécula se ligou a algo maior. No contexto da computação quântica, geralmente queremos que T2T_2 e T2∗T_2^* sejam o mais longos possível, para termos mais tempo de realizar experimentos antes que a decoerência domine o sistema. Toda uma família de técnicas baseadas no eco de Hahn amplia esse tempo cancelando o ruído reversível, de modo que o circuito fique idealmente limitado pelo T2T_2 verdadeiro, e não por T2∗T_2^*. Isso é chamado de "desacoplamento dinâmico".

Implementação​

Quando implementamos os experimentos de Ramsey e de eco de Hahn em um qubit, a leitura é um pouco diferente da do caso da RMN. Em RMN, você pode observar diretamente os campos oscilantes induzidos pelos spins que precessam à frequência de Larmor no plano transversal. Mas com um qubit, só podemos medir sua projeção ao longo do eixo ZZ, isto é, se ele está em ∣0⟩\vert 0 \rangle ou ∣1⟩\vert 1 \rangle. Portanto, em ambos os experimentos, terminamos com um pulso π/2\pi/2 adicional seguido de uma medição. Esse pulso π/2\pi/2 converte a informação de fase do qubit em uma população.

A seguir, executamos cada experimento para determinar T2∗T_2^* e T2T_2.

Ramsey (T2∗T_2^{*}): dois pulsos π/2\pi/2 com tempo de atraso τ\tau​

O primeiro pulso π/2\pi/2 leva o vetor de Bloch até o equador, e o seguinte converte a fase do vetor de Bloch em uma população. Como a fase muda no tempo conforme o vetor de Bloch precessa, vemos um padrão senoidal na população em função do tempo de precessão τ\tau. À medida que o qubit perde a coerência de fase, devemos ver a amplitude dessa curva senoidal decair.

Mais algumas sutilezas para leitores de computação quântica: Em experimentos de computação quântica, as oscilações que vemos não estão, na verdade, na frequência de Larmor, que fica na faixa de GHz e é rápida demais para resolvermos. Os experimentos quânticos operam no referencial girante: um referencial que gira junto com o qubit a (quase) a frequência de Larmor, de modo que a precessão rápida fica invisível e o vetor de Bloch parece quase parado. Para ver franjas, deliberadamente ajustamos nosso referencial girante com uma pequena dessintonia em relação à frequência de Larmor. O vetor de Bloch então parece precessar a essa pequena dessintonia, e obtemos um sinal oscilante na frequência da dessintonia. A seguir, não fixamos a dessintonia em um único valor; nós a escolhemos de modo que cerca de uma dúzia de franjas caiba na janela de medição (que dimensionamos de acordo com o tempo de coerência do qubit), o que mantém a oscilação fácil de amostrar sem aliasing. Mais uma peculiaridade: no Qiskit não temos o controle em nível de pulso necessário para mudar a frequência do pulso (que é o que define a frequência do referencial girante). Em vez disso, adicionamos uma dessintonia efetiva manualmente, usando-a para definir a fase do segundo pulso de Ramsey. É um pouco artificial, mas o resultado é o mesmo e o decaimento que observamos continua sendo o genuíno T2∗T_2^*.

Uma visão na esfera de Bloch​

Um pulso π/2\pi/2 leva os spins ao equador; cada um então precessa a uma taxa ligeiramente diferente e eles se espalham em leque, de modo que o vetor líquido (vermelho) encolhe à medida que se defasam — esse decaimento é 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): adicione um pulso π\pi de refocalização no meio​

Uma visão na esfera de Bloch​

O eco de Hahn: os spins se espalham em leque como antes, então um pulso π\pi os inverte, de modo que sua precessão contínua os refocaliza novamente juntos (o vetor vermelho volta a crescer), recuperando a coerência perdida para a defasagem estática.

# 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

Analisar: ajustar e comparar os dois decaimentos​

# 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

Aqui vemos tanto a oscilação de Ramsey quanto o sinal do eco de Hahn decaírem, como esperado. As escalas de tempo associadas a cada um desses decaimentos são T2∗T_2^* e T2T_2, respectivamente.

Verifique seu entendimento​

Você deve encontrar tipicamente T2≳T2∗T_2 \gtrsim T_2^{*}. Explique por quê em uma frase e diga qual efeito físico o pulso π\pi do eco de Hahn remove, bem como o que ele não consegue remover.

Resposta

T2∗T_2^{*} inclui tanto a defasagem irreversível quanto a dispersão estática reversível, então decai mais rápido; o pulso π\pi do eco refocaliza a defasagem estática e reversível (inomogeneidade do campo / deslocamentos de frequência fixos), deixando apenas a defasagem spin-spin irreversível, que é o T2T_2 verdadeiro. O eco não consegue desfazer flutuações que mudam entre as duas metades da sequência.

RMN de uma molécula: deslocamento químico e acoplamento JJ​

De um spin a uma molécula​

Os experimentos até agora caracterizaram um único spin isolado. A RMN química real lê uma molécula (muitos spins acoplados ao mesmo tempo) e usa o espectro resultante para inferir características da estrutura da molécula. Isso pode nos informar sobre dois efeitos:

  • Deslocamento químico. Cada núcleo quimicamente distinto precessa a uma frequência ligeiramente diferente νi\nu_i, porque seus elétrons locais blindam o campo aplicado em quantidades diferentes. As posições dos picos, portanto, identificam ambientes químicos (um próton de CH3_3 versus um aromático, por exemplo).

  • Acoplamento escalar JJ. Uma interação através das ligações entre dois spins divide cada pico em um multipleto. Um núcleo com nn vizinhos equivalentes é dividido em n+1n+1 linhas com intensidades binomiais, a regra do n+1n+1. O padrão de divisão revela a conectividade (quais núcleos estão ligados), e o espaçamento é a constante de acoplamento JJ.

O experimento​

Para modelar isso em nosso computador quântico, precisamos implementar um Hamiltoniano de spin nos qubits. No limite de acoplamento fraco e campo alto (diferenças de deslocamento químico ≫\gg acoplamentos), as partes XXXX e YYYY do acoplamento se anulam na média, restando apenas ZZZZ. Assim, o Hamiltoniano é (em unidades de frequência 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 é um qubit: o deslocamento químico é uma rotação ZZ de um qubit, e o acoplamento é uma interação ZZZZ de dois qubits.

O experimento prossegue da seguinte forma. Um pulso π/2\pi/2 leva cada spin ao plano transversal, e os spins precessam e se acoplam durante o tempo tt. Registramos a magnetização transversal complexa s(t)=⟨X⟩+i⟨Y⟩s(t) = \langle X\rangle + i\langle Y\rangle somada sobre todos os spins. Como no experimento de Ramsey acima, essa magnetização transversal traça uma senoide decrescente em função do tempo de precessão tt. Quando tomamos a transformada de Fourier do sinal, obtemos um espectro com picos no deslocamento químico νi\nu_i divididos pelo acoplamento escalar JijJ_{ij}.

nota

Simulamos a evolução da molécula mapeando o tempo tt em ângulos de rotação: um tt maior significa rotações ZZ e ZZZZ maiores, não um circuito mais longo ou mais profundo. Assim, embora o estado simulado de fato avance com tt (é isso que faz o decaimento de indução livre (FID) oscilar), os qubits físicos nunca ficam ociosos por mais tempo e não há decoerência dependente de tt. O FID simulado, portanto, nunca decai por conta própria. Para um FID ideal infinitamente longo, as linhas espectrais seriam infinitamente nítidas; nossa janela de amostragem finita, em vez disso, lhes dá resolução finita e pode introduzir vazamento espectral. As linhas reais de RMN têm uma largura finita definida por T2∗T_2^*. Nós a adicionamos de volta multiplicando o FID por uma janela e−t/T2∗e^{-t/T_2^{*}} antes da transformada de Fourier. Isso é chamado de apodização de alargamento de linha, que os espectroscopistas aplicam no processamento para definir a largura de linha (trocando resolução por relação sinal-ruído). (Uma célula opcional no Apêndice deste módulo mostra uma alternativa Trotterizada, em que o decaimento vem, em vez disso, da decoerência real que cresce com a profundidade do circuito.)

O sistema AX​

Vamos começar com o chamado sistema AX, uma molécula formada por dois spins acoplados e quimicamente distintos. Os rótulos A e X denotam os dois ambientes de spin quimicamente distintos. As letras são arbitrárias, mas o fato de estarem distantes no alfabeto ilustra sua dissimilaridade química.

Mapear e otimizar​

Escrevemos uma função que, dados os deslocamentos químicos νi\nu_i e os acoplamentos JijJ_{ij}, retorna um circuito parametrizado de preparação de estado. O pulso π/2\pi/2 leva os spins ao equador, e então as rotações ZZ simulam a evolução do estado sob os termos de deslocamento químico e de acoplamento JJ. Cada νi\nu_i é um pequeno deslocamento de precessão em Hz relativo a uma referência escolhida, ou portadora do referencial girante. Aqui o definimos manualmente; em uma molécula real, a blindagem eletrônica altera o campo magnético local e desloca a ressonância de cada núcleo em relação a essa referência. Isso é modelado pelas portas RZR_Z de um qubit, uma por spin, e cada νi\nu_i fixa onde o pico daquele spin fica no espectro. A porta ZZZZ de dois qubits aplica o acoplamento JijJ_{ij}, que divide cada pico em um multipleto e define o espaçamento entre suas linhas.

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

Executar e pós-processar​

Avaliamos o circuito sobre uma grade de tempos de evolução, e o Estimator retorna ⟨X⟩\langle X\rangle e ⟨Y⟩\langle Y\rangle em cada um. Combinamos esses valores no FID complexo ⟨X⟩+i⟨Y⟩\langle X\rangle + i\langle Y\rangle, aplicamos a janela de apodização e fazemos a transformada de Fourier do resultado para obter o 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]

Extensão: um sistema AX2_2 e a regra do n+1n+1​

Agora adicione um segundo vizinho equivalente. Em um sistema AX2_2, um spin A se acopla, com o mesmo JJ, a dois spins X equivalentes. (Lembre-se: X é apenas um rótulo para os dois spins equivalentes; não tem nada a ver com os operadores de Pauli XX. Todo acoplamento abaixo continua sendo ZZZZ.) O spin A então vê quatro configurações de vizinhos (↑↑, ↑↓, ↓↑, ↓↓)(\uparrow\uparrow,\ \uparrow\downarrow,\ \downarrow\uparrow,\ \downarrow\downarrow); as duas mistas coincidem, então A é dividido em um tripleto 1:2:1 (n=2⇒n+1=3n=2 \Rightarrow n+1 = 3 linhas). Cada spin X vê apenas A e permanece um dubleto. Spins equivalentes não se dividem entre si, então não incluímos acoplamento X–X.

Uma molécula real com esse padrão é o 1,1,2-tricloroetano (Cl2_2CH–CH2_2Cl). Ao observar os spins do hidrogênio (que é a forma mais comum de RMN), o único próton de hidrogênio em CH faz o papel de A, e os dois prótons equivalentes de CH2_2 são os dois spins X. Agora basta contar os vizinhos: o próton de CH tem dois vizinhos (o par CH2_2), então seu sinal se divide em um tripleto 1:2:1 (n=2n=2); cada próton de CH2_2 tem apenas um vizinho (o CH), então o sinal de CH2_2 é um dubleto 1:1 (n=1n=1). Note que o grupo com menos prótons (CH) mostra mais linhas; o número de linhas reflete quantos vizinhos um sinal tem, não quantos prótons o compartilham.

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

A seguir, simulamos um sistema AX2_2 com três 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)],
)

Verifique seu entendimento​

Leia cada pergunta, pense na sua resposta e então clique no triângulo para revelar a solução.

No espectro AX, os dois dubletos são separados (de centro a centro) por 12 Hz, e as duas linhas dentro de cada dubleto estão a 2 Hz uma da outra. Qual número é a diferença de deslocamento químico e qual é a constante de acoplamento JJ? Se você passasse para um espectrômetro com o dobro do campo magnético, qual deles mudaria?

Resposta

A separação de 12 Hz entre os centros é a diferença de deslocamento químico (ν1−ν2\nu_1-\nu_2); o espaçamento de 2 Hz entre as linhas é a constante de acoplamento JJ. Os deslocamentos químicos vêm da blindagem de B0B_0, então escalam com o campo. Com o dobro do campo, a separação dobra para 24 Hz. O acoplamento JJ é uma propriedade molecular através das ligações e independe do campo, então permanece 2 Hz. Os químicos distinguem deslocamento de acoplamento observando como uma divisão escala com o campo.

Usando a regra do n+1n+1, preveja o multipleto de A se um terceiro spin X equivalente fosse adicionado (um sistema AX3_3), e explique por que os dois spins X equivalentes em AX2_2 não produzem divisão um no outro.

Resposta

Com n=3n=3 vizinhos equivalentes, A se torna um quarteto 1:3:3:1. Spins equivalentes não se dividem entre si porque seu acoplamento desloca os níveis de energia relevantes todos na mesma quantidade, então as diferenças entre esses níveis — as frequências que vemos — não mudam.

Conclusão​

Usamos um computador quântico real para explorar como os spins nucleares precessam e decoerem em um experimento de RMN, e como interpretar os espectros resultantes. Vale dar um passo atrás para rever por que isso foi possível e por que é mais do que uma curiosidade.

Um qubit e um núcleo de spin 1/2 são, matematicamente, o mesmo objeto: sistemas quânticos de dois níveis que vivem na esfera de Bloch, são girados por pulsos ressonantes, precessam em torno de um eixo e perdem coerência para o ambiente. Todo o vocabulário deste módulo (a frequência de Larmor, o referencial girante, T1T_1, T2T_2 e T2∗T_2^*, as franjas de Ramsey, o eco de Hahn) pertence igualmente à RMN e à computação quântica. O que um químico chama de deslocamento químico, um engenheiro quântico chama de rotação ZZ de um qubit; o que um químico chama de acoplamento JJ, um engenheiro quântico chama de interação ZZZZ. As duas áreas vêm descrevendo a mesma física com palavras diferentes.

Essa conexão é histórica, não apenas conceitual: a RMN foi uma das primeiras plataformas a executar algoritmos quânticos reais. Em 2001, uma molécula em solução executou o algoritmo de Shor para fatorar 15, com cada spin nuclear servindo como um qubit. A RMN também legou à computação quântica grande parte de seu conjunto de ferramentas de controle. O eco de spin, os trens de pulsos CPMG, os pulsos compostos e o desacoplamento dinâmico foram todos inventados por espectroscopistas décadas antes de os qubits precisarem deles para combater a decoerência. Há uma simetria satisfatória no que acabamos de fazer: o campo que deu origem aos primeiros computadores quânticos agora é algo que um computador quântico pode simular.

Revisão e perguntas​

Conceitos críticos​

  • Um qubit é um sistema de dois níveis projetado, equivalente a um spin 1/2; seus ∣0⟩/∣1⟩|0\rangle/|1\rangle, frequência, portas, T1T_1 e T2T_2 são os análogos diretos das grandezas da RMN.

  • Um pulso ressonante gira o vetor de Bloch; o ângulo de rotação define a rotação, e a curva de Rabi/nutação P1=sin⁡2(θ/2)P_1=\sin^2(\theta/2) identifica as rotações π\pi e π/2\pi/2.

  • T1T_1 é a relaxação de energia (spin-rede); T2T_2 é a relaxação de fase (spin-spin); T2∗T_2^{*} inclui adicionalmente a defasagem estática reversível que um eco de Hahn refocaliza.

  • Uma molécula é um conjunto de spins acoplados: o deslocamento químico define a posição de cada pico, e o acoplamento JJ divide os picos em multipletos seguindo a regra do n+1n+1, que juntos codificam o ambiente químico e a conectividade.

Verdadeiro / falso​

  1. V/F A frequência de Larmor é a diferença de energia entre os dois estados de spin dividida por ℏ\hbar.

  2. V/F Um pulso de 90∘90^\circ (π/2\pi/2) inverte completamente um spin em equilíbrio.

  3. V/F T2∗T_2^{*} é geralmente mais longo que T2T_2.

  4. V/F O eco de Hahn refocaliza a defasagem spin-spin irreversível.

  5. V/F Em um sistema AX2_2, o núcleo A aparece como um tripleto 1:2:1.

  6. V/F A constante de acoplamento JJ dobra quando você dobra o campo magnético do espectrômetro.

  7. V/F Remover o Qiskit Pulse torna impossível medir T1T_1 no hardware atual.

Resposta curta​

  1. Explique em duas frases por que um único pulso π\pi é uma maneira ruim de detectar um erro de ângulo de rotação de 5%5\% e como encadear pulsos resolve isso.

  2. Uma amostra apresenta linhas de RMN largas. Em termos de T2T_2, o que isso lhe diz e qual grandeza do qubit é o análogo?

  3. Dê o análogo em qubits de cada item: (a) o atraso de reciclagem, (b) o pulso de 90∘90^\circ, (c) o deslocamento químico.

  4. Usando a regra do n+1n+1, explique por que um grupo etila (CH3_3CH2_2–) mostra um tripleto 1:2:1 e um quarteto 1:3:3:1, e identifique qual grupo produz qual multipleto.

Problemas desafio​

  1. Trem de Carr-Purcell. Modifique o eco de Hahn para inserir nn pulsos π\pi igualmente espaçados (um trem no estilo CPMG) e meça T2T_2 em função de nn. Discuta por que mais pulsos de refocalização podem alongar o T2T_2 aparente.

  2. Amplificação de erro de pulso. Construa um circuito que aplica RX(π)R_X(\pi)  n\,n vezes a ∣0⟩|0\rangle e faça o gráfico de P1P_1 versus nn para um ângulo deliberadamente mal ajustado RX(1.05π)R_X(1.05\pi). Mostre como o erro se torna visível conforme nn cresce. Dica: coloque uma barreira após cada rotação; caso contrário, o transpilador pode combinar as rotações repetidas em uma única porta equivalente.

  3. Grupo etila. Usando fid_circuit, construa o espectro de prótons de um fragmento etila (CH3_3–CH2_2): cinco qubits, com os três prótons de CH3_3 compartilhando um deslocamento químico e os dois prótons de CH2_2 outro, e cada par CH3_3–CH2_2 acoplado pelo mesmo JJ (sem acoplamento dentro de um grupo equivalente). Preveja o padrão com a regra do n+1n+1 e depois simule-o; você deve recuperar um quarteto 1:3:3:1 (CH2_2) e um tripleto 1:2:1 (CH3_3).

Apêndice (opcional): evolução Trotterizada — deixe a decoerência definir a largura de linha​

Na seção RMN de uma molécula, a evolução e−iHte^{-iHt} é aplicada em uma única camada cuja profundidade não cresce com tt, então o FID não decai por conta própria e impomos a largura de linha com a janela de apodização e−t/T2e^{-t/T_2}. Esta célula opcional adota a abordagem oposta. Ela divide a evolução em pp passos de Trotter e coloca uma barreira após cada passo (para que o transpilador não possa fundi-los de volta em uma única camada). O ponto de tempo kk usa kk passos, então a profundidade do circuito cresce com o tempo.

Como todos os termos em HH comutam, isso é exato: em um simulador sem ruído, ele dá os mesmos picos nítidos. Mas em um backend com modelo de ruído ou em hardware real, o erro de porta acumulado faz o FID decair por conta própria, e esse decaimento é o que define a largura de linha. Esta é a ideia de T2↔T_2\leftrightarrowlargura de linha da seção experimento T2T_2, T2∗T_2^*, agora surgindo da decoerência real em vez de uma janela de processamento. Executamos dessa forma tanto o sistema AX (dois dubletos) quanto o sistema AX2_2 (um tripleto 1:2:1 mais um dubleto).

Duas observações práticas a considerar. Primeiro, o custo é a profundidade: o circuito AX mais profundo usa ∼ ⁣47\sim\!47 portas de dois qubits e a versão AX2_2 cerca do dobro disso, então ambos são muito mais ruidosos e caros que a versão exata. Em segundo lugar, cada sistema de spin ainda é enviado como um único job, e cada ponto de tempo reutiliza o mesmo layout de qubits físicos selecionado na seção de espectroscopia. Se USE_BATCH=True, esses jobs também usam o Batch opcional de todo o 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()