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:
qiskitv2.1 ou mais recenteqiskit-ibm-runtimev0.40.1 ou mais recenteqiskit-aerv0.17 ou mais recentescipyematplotlib(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, e .
Um qubit supercondutor é uma versão projetada do mesmo objeto: um sistema quântico controlável de dois níveis. Seus estados e 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 e . 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 ou .
-
— relaxação spin-rede, o decaimento de um spin excitado de volta ao equilíbrio.
-
— 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 ) — simular uma molécula pequena em vários qubits e ler seu espectro de multipletos para recuperar a conectividade por meio da regra .
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 , em que é a razão giromagnética. Em um campo estático ao longo de , os dois estados de spin se separam em energia (o efeito Zeeman):
A diferença de energia define a frequência de Larmor (de ressonância) . O momento magnético precessiona em torno de nessa mesma frequência.
Para girar o spin, aplicamos um campo de RF oscilante em (ou perto de) . 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 -. 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 inclina um spin para o plano transversal, e um pulso de o gira completamente para .
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/2 | Processador quântico (qubit) |
|---|---|
| Spin para cima / spin para baixo | / |
| Gap de Zeeman | Energia de transição do qubit |
| Frequência de Larmor | Frequência do qubit |
| Pulso de RF ressonante (ângulo de rotação ) | Porta de rotação de um único qubit |
| Pulso de / | / |
| Relaxação spin-rede | Tempo de relaxação de energia do qubit |
| Relaxação spin-spin | Tempo de defasagem do qubit |
| Nutação | Oscilações de Rabi |
| Decaimento de indução livre (FID), eco de Hahn | Sequê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:
-
Mapear o problema para um circuito quântico.
-
Otimizar o circuito para execução (transpilar para o backend).
-
Executar usando uma primitiva do IBM Quantum.
-
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 em torno do eixo a um qubit que começa em , a probabilidade de encontrá-lo em é:
Esta é a curva de Rabi. Seu primeiro máximo é o pulso (inversão completa, o pulso de ); um pulso é um quarto de volta (o pulso de que inclina um spin para o plano transversal). Na RMN, essa curva é como você calibra seus pulsos.
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 de uma porta . A curva e a identificação de / 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, para 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 , levando-o de em direção a e de volta. Este é exatamente o movimento que a curva de Rabi mede.
Mapeie o problema para um circuito
Construímos um circuito parametrizado, 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")
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 de a (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 em cada ângulo e compare com .
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()

Verifique seu entendimento
Na curva que você mediu, atinge seu máximo pela primeira vez no pulso (). Agora suponha que um dispositivo esteja descalibrado, de modo que um ângulo nominal no eixo x produza, na verdade, uma rotação de .
(a) Em que ângulo nominal o primeiro máximo de apareceria agora? (b) Aplicar um único pulso seria uma maneira sensível de detectar esse erro de 5%?
Resposta
(a) O máximo aparece onde a rotação real é igual a , ou seja, no ângulo nominal — um pequeno deslocamento do pico para a esquerda.
(b) Não. Um pulso nominal produz uma rotação real de , resultando em em vez de — 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 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: (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 :
Para um químico, 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 com uma porta (um pulso ), espere um atraso variável e então meça. A fração que ainda está em traça o decaimento.
Uma visão na esfera de Bloch
A relaxação leva o estado excitado de volta ao equilíbrio, de modo que o vetor de Bloch desliza de de volta para ao longo do eixo .
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")
Measured T1 = 225.3 microseconds
O tempo 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 entre varreduras. Usando a lei de decaimento, que fração da magnetização de equilíbrio se recuperou após ?
Resposta
Após , o desvio restante do equilíbrio é , ou seja, cerca de 99,3% recuperado.
Experimento: ,
Teoria
mede a perda de energia. mede a perda de coerência de fase no plano transversal. Podemos observar isso com um experimento de Ramsey: um pulso 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 .
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 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 verdadeiro, eles produzem o tempo de decaimento aparente mais curto observado em um experimento de Ramsey.
Então, como recuperamos o verdadeiro se um experimento de Ramsey só nos informa sobre ? O segundo experimento, chamado de eco de Hahn, cancela todo o ruído do segundo tipo para encontrar o verdadeiro. Ele começa igual a um experimento de Ramsey, com um pulso , e então os spins se espalham em leque como antes. Após algum tempo , um pulso é 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, .
e 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, define a largura de linha de um experimento de RMN padrão (você verá o que isso significa em uma seção posterior). pode dizer muito sobre as moléculas, como o movimento molecular de rotação, e uma mudança em pode sinalizar que uma molécula se ligou a algo maior. No contexto da computação quântica, geralmente queremos que e 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 verdadeiro, e não por . 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 , isto é, se ele está em ou . Portanto, em ambos os experimentos, terminamos com um pulso adicional seguido de uma medição. Esse pulso converte a informação de fase do qubit em uma população.
A seguir, executamos cada experimento para determinar e .
Ramsey (): dois pulsos com tempo de atraso
O primeiro pulso 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 . À 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 .
Uma visão na esfera de Bloch
Um pulso 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 é .
# Adaptive Ramsey: scale the window to the reported coherence and choose the detuning so a fixed
# number of fringes span it. This decouples the point count from the window -- a long window stays
# cheap and cannot alias (we keep 8 samples per fringe).
T2_us = (backend.qubit_properties(QUBIT).t2 or 100e-6) * 1e6 # reported (echo) T2 (us)
ramsey_span = 0.5 * T2_us # window = half the reported T2
n_fringes = 12 # fringes across the window
f_detune = n_fringes / (
ramsey_span * 1e-6
) # adaptive detuning (Hz) -> low, so a long window is cheap
samp_per_fringe = 8
n_ramsey = n_fringes * samp_per_fringe + 1 # ~97 points, fixed regardless of T2
ramsey_delays_us = np.linspace(0, ramsey_span, n_ramsey)
print(
f"Ramsey sweep: 0..{ramsey_span:.0f} us in {n_ramsey} points "
f"(detuning {f_detune/1e3:.1f} kHz, {samp_per_fringe} samples/fringe)"
)
dt = backend.dt # seconds per sample
delays_dt = [int(round(t * 1e-6 / dt)) for t in ramsey_delays_us] # integer samples
actual_delays_us = np.array(delays_dt) * dt * 1e6 # true (snapped) delays for fitting
ramsey_circ = []
for n_dt, tau_us in zip(delays_dt, actual_delays_us):
qc = QuantumCircuit(1, 1)
qc.rx(np.pi / 2, 0)
qc.delay(n_dt, 0, unit="dt") # integer duration -> no rounding warning
qc.rz(
2 * np.pi * f_detune * tau_us * 1e-6, 0
) # detuning phase (uses the true delay)
qc.rx(np.pi / 2, 0)
qc.measure(0, 0)
ramsey_circ.append(qc)
ramsey_isa = pm_q.run(ramsey_circ)
ramsey_res = sampler.run([(c,) for c in ramsey_isa], shots=1000).result()
p1_ramsey = np.array(
[r.data.c.get_counts().get("1", 0) / r.data.c.num_shots for r in ramsey_res]
)
ramsey_circ[len(ramsey_circ) // 2].draw("mpl") # draw a sample circuit (mid-sweep)
Ramsey sweep: 0..248 us in 97 points (detuning 48.4 kHz, 8 samples/fringe)
Eco de Hahn (): adicione um pulso 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 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
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")

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 e , respectivamente.
Verifique seu entendimento
Você deve encontrar tipicamente . Explique por quê em uma frase e diga qual efeito físico o pulso do eco de Hahn remove, bem como o que ele não consegue remover.
Resposta
inclui tanto a defasagem irreversível quanto a dispersão estática reversível, então decai mais rápido; o pulso 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 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
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 , 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 CH versus um aromático, por exemplo).
-
Acoplamento escalar . Uma interação através das ligações entre dois spins divide cada pico em um multipleto. Um núcleo com vizinhos equivalentes é dividido em linhas com intensidades binomiais, a regra do . O padrão de divisão revela a conectividade (quais núcleos estão ligados), e o espaçamento é a constante de acoplamento .
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 acoplamentos), as partes e do acoplamento se anulam na média, restando apenas . Assim, o Hamiltoniano é (em unidades de frequência angular, ):
Cada núcleo é um qubit: o deslocamento químico é uma rotação de um qubit, e o acoplamento é uma interação de dois qubits.
O experimento prossegue da seguinte forma. Um pulso leva cada spin ao plano transversal, e os spins precessam e se acoplam durante o tempo . Registramos a magnetização transversal complexa 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 . Quando tomamos a transformada de Fourier do sinal, obtemos um espectro com picos no deslocamento químico divididos pelo acoplamento escalar .
Simulamos a evolução da molécula mapeando o tempo em ângulos de rotação: um maior significa rotações e maiores, não um circuito mais longo ou mais profundo. Assim, embora o estado simulado de fato avance com (é 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 . 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 . Nós a adicionamos de volta multiplicando o FID por uma janela 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 e os acoplamentos , retorna um circuito parametrizado de preparação de estado. O pulso leva os spins ao equador, e então as rotações simulam a evolução do estado sob os termos de deslocamento químico e de acoplamento . Cada é 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 de um qubit, uma por spin, e cada fixa onde o pico daquele spin fica no espectro. A porta de dois qubits aplica o acoplamento , 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 e em cada um. Combinamos esses valores no FID complexo , 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)}),
)

Expected AX lines (Hz): [-7.0, -5.0, 5.0, 7.0]
Extensão: um sistema AX e a regra do
Agora adicione um segundo vizinho equivalente. Em um sistema AX, um spin A se acopla, com o mesmo , 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 . Todo acoplamento abaixo continua sendo .) O spin A então vê quatro configurações de vizinhos ; as duas mistas coincidem, então A é dividido em um tripleto 1:2:1 ( 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 (ClCH–CHCl). 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 CH são os dois spins X. Agora basta contar os vizinhos: o próton de CH tem dois vizinhos (o par CH), então seu sinal se divide em um tripleto 1:2:1 (); cada próton de CH tem apenas um vizinho (o CH), então o sinal de CH é um dubleto 1: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.
A seguir, simulamos um sistema AX 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 ? 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 (); o espaçamento de 2 Hz entre as linhas é a constante de acoplamento . Os deslocamentos químicos vêm da blindagem de , então escalam com o campo. Com o dobro do campo, a separação dobra para 24 Hz. O acoplamento é 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 , preveja o multipleto de A se um terceiro spin X equivalente fosse adicionado (um sistema AX), e explique por que os dois spins X equivalentes em AX não produzem divisão um no outro.
Resposta
Com 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, , e , 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 de um qubit; o que um químico chama de acoplamento , um engenheiro quântico chama de interação . 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 , frequência, portas, e 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 identifica as rotações e .
-
é a relaxação de energia (spin-rede); é a relaxação de fase (spin-spin); 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 divide os picos em multipletos seguindo a regra do , que juntos codificam o ambiente químico e a conectividade.
Verdadeiro / falso
-
V/F A frequência de Larmor é a diferença de energia entre os dois estados de spin dividida por .
-
V/F Um pulso de () inverte completamente um spin em equilíbrio.
-
V/F é geralmente mais longo que .
-
V/F O eco de Hahn refocaliza a defasagem spin-spin irreversível.
-
V/F Em um sistema AX, o núcleo A aparece como um tripleto 1:2:1.
-
V/F A constante de acoplamento dobra quando você dobra o campo magnético do espectrômetro.
-
V/F Remover o Qiskit Pulse torna impossível medir no hardware atual.
Resposta curta
-
Explique em duas frases por que um único pulso é uma maneira ruim de detectar um erro de ângulo de rotação de e como encadear pulsos resolve isso.
-
Uma amostra apresenta linhas de RMN largas. Em termos de , o que isso lhe diz e qual grandeza do qubit é o análogo?
-
Dê o análogo em qubits de cada item: (a) o atraso de reciclagem, (b) o pulso de , (c) o deslocamento químico.
-
Usando a regra do , explique por que um grupo etila (CHCH–) mostra um tripleto 1:2:1 e um quarteto 1:3:3:1, e identifique qual grupo produz qual multipleto.
Problemas desafio
-
Trem de Carr-Purcell. Modifique o eco de Hahn para inserir pulsos igualmente espaçados (um trem no estilo CPMG) e meça em função de . Discuta por que mais pulsos de refocalização podem alongar o aparente.
-
Amplificação de erro de pulso. Construa um circuito que aplica vezes a e faça o gráfico de versus para um ângulo deliberadamente mal ajustado . Mostre como o erro se torna visível conforme 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.
-
Grupo etila. Usando
fid_circuit, construa o espectro de prótons de um fragmento etila (CH–CH): cinco qubits, com os três prótons de CH compartilhando um deslocamento químico e os dois prótons de CH outro, e cada par CH–CH acoplado pelo mesmo (sem acoplamento dentro de um grupo equivalente). Preveja o padrão com a regra do e depois simule-o; você deve recuperar um quarteto 1:3:3:1 (CH) e um tripleto 1:2:1 (CH).
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 é aplicada em uma única camada cuja profundidade não cresce com , então o FID não decai por conta própria e impomos a largura de linha com a janela de apodização . Esta célula opcional adota a abordagem oposta. Ela divide a evolução em 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 usa passos, então a profundidade do circuito cresce com o tempo.
Como todos os termos em 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 largura de linha da seção experimento , , 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 AX (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 portas de dois qubits e a versão AX 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()