Pulsed Experiments

This tutorial covers common pulsed measurement sequences.

Rabi Oscillations

Measure the Rabi frequency by varying pulse duration:

import numpy as np
import matplotlib.pyplot as plt
from sim import HamiltonianBuilder, LindbladSolver
from sim.hamiltonian.terms import ZFS, MicrowaveDrive
from sim.states import ground_state, projector_ms

# Rabi frequency
omega_rabi = 10  # MHz

durations = np.linspace(0, 200e-9, 100)  # 0 to 200 ns
populations = []

for t_pulse in durations:
    # Time-dependent MW
    def mw_amplitude(t):
        return omega_rabi if t < t_pulse else 0

    H = HamiltonianBuilder()
    H.add(ZFS(D=2.87))
    H.add(MicrowaveDrive(omega=mw_amplitude))

    solver = LindbladSolver(H)
    solver.add_t2_dephasing(gamma=1e5)

    rho0 = ground_state()
    result = solver.evolve(rho0, (0, t_pulse + 1e-9), n_steps=50)

    pop = np.trace(projector_ms(+1) @ result.final_state()).real
    populations.append(pop)

plt.figure(figsize=(10, 5))
plt.plot(durations * 1e9, populations, 'b-')
plt.xlabel("Pulse Duration (ns)")
plt.ylabel("Population ms=+1")
plt.title(f"Rabi Oscillations (Ω = {omega_rabi} MHz)")

# Expected: cos²(Ωt/2) with period T = 2π/Ω = 100 ns
t_theory = durations * 1e9
pop_theory = np.sin(np.pi * omega_rabi * durations * 1e6)**2
plt.plot(t_theory, pop_theory, 'r--', label='Theory')
plt.legend()
plt.show()

Ramsey Interferometry

Measure T₂* and detuning:

from sim.pulses.sequences import ramsey_sequence

tau_values = np.linspace(0, 5e-6, 100)  # 0 to 5 μs
signal = []

# Small detuning to see oscillations
detuning = 1  # MHz

for tau in tau_values:
    H = HamiltonianBuilder()
    H.add(ZFS(D=2.87))
    H.add(MicrowaveDrive(omega=20, detuning=detuning))  # Fast π/2

    solver = LindbladSolver(H)
    solver.add_t2_dephasing(gamma=0.5e6)  # T2* = 2 μs

    rho = ground_state()

    # First π/2
    t_pi2 = 12.5e-9  # 12.5 ns for 20 MHz Rabi
    result = solver.evolve(rho, (0, t_pi2), n_steps=10)
    rho = result.final_state()

    # Free evolution (no MW)
    H_free = HamiltonianBuilder()
    H_free.add(ZFS(D=2.87))
    solver_free = LindbladSolver(H_free)
    solver_free.add_t2_dephasing(gamma=0.5e6)
    result = solver_free.evolve(rho, (0, tau), n_steps=20)
    rho = result.final_state()

    # Second π/2
    result = solver.evolve(rho, (0, t_pi2), n_steps=10)
    rho = result.final_state()

    pop = np.trace(projector_ms(0) @ rho).real
    signal.append(pop)

plt.figure(figsize=(10, 5))
plt.plot(tau_values * 1e6, signal, 'b-')
plt.xlabel("Free Evolution Time τ (μs)")
plt.ylabel("Population ms=0")
plt.title("Ramsey Fringes")

# Fit: A * exp(-τ/T2*) * cos(2π*Δ*τ) + offset
plt.show()

Spin Echo (Hahn Echo)

Measure T₂ by refocusing static noise:

tau_values = np.linspace(0, 500e-6, 50)  # 0 to 500 μs
echo_signal = []

for tau in tau_values:
    H = HamiltonianBuilder()
    H.add(ZFS(D=2.87))
    H.add(MicrowaveDrive(omega=20))

    solver = LindbladSolver(H)
    # Pure dephasing (refocusable)
    solver.add_t2_dephasing(gamma=1e6)
    # T1 relaxation (not refocusable)
    solver.add_t1_relaxation(gamma=100)

    rho = ground_state()
    t_pi2 = 12.5e-9
    t_pi = 25e-9

    # π/2
    result = solver.evolve(rho, (0, t_pi2), n_steps=5)
    rho = result.final_state()

    # τ/2 free evolution
    H_free = HamiltonianBuilder()
    H_free.add(ZFS(D=2.87))
    solver_free = LindbladSolver(H_free)
    solver_free.add_t2_dephasing(gamma=1e6)
    solver_free.add_t1_relaxation(gamma=100)
    result = solver_free.evolve(rho, (0, tau/2), n_steps=10)
    rho = result.final_state()

    # π refocusing pulse
    result = solver.evolve(rho, (0, t_pi), n_steps=5)
    rho = result.final_state()

    # τ/2 free evolution
    result = solver_free.evolve(rho, (0, tau/2), n_steps=10)
    rho = result.final_state()

    # π/2 readout
    result = solver.evolve(rho, (0, t_pi2), n_steps=5)
    rho = result.final_state()

    pop = np.trace(projector_ms(0) @ rho).real
    echo_signal.append(pop)

plt.figure(figsize=(10, 5))
plt.plot(tau_values * 1e6, echo_signal, 'b-o')
plt.xlabel("Total Evolution Time τ (μs)")
plt.ylabel("Echo Amplitude")
plt.title("Spin Echo Decay")
plt.show()

CPMG Dynamical Decoupling

Extend coherence with multiple refocusing pulses:

n_pulses_list = [1, 2, 4, 8, 16, 32]
tau_fixed = 100e-6  # Total evolution time

coherence_vs_n = []

for n in n_pulses_list:
    tau_segment = tau_fixed / (2 * n)

    # ... similar pulse sequence with n π-pulses ...
    # (Implementation similar to spin echo but with loop)

    # Placeholder for demonstration
    # In reality, more pulses = better coherence preservation
    T2_eff = 200e-6 * np.sqrt(n)  # Approximate scaling
    coherence = np.exp(-tau_fixed / T2_eff)
    coherence_vs_n.append(coherence)

plt.figure(figsize=(8, 5))
plt.semilogy(n_pulses_list, coherence_vs_n, 'bo-')
plt.xlabel("Number of π-pulses")
plt.ylabel("Coherence")
plt.title("CPMG: Coherence vs Number of Pulses")
plt.grid(True)
plt.show()

XY8 Sequence for AC Sensing

The XY8 sequence is sensitive to AC fields at specific frequencies:

# Sensing frequency f_sense
# Maximum sensitivity when τ = 1/(2*f_sense)

f_sense = 1e6  # 1 MHz AC field
tau = 1 / (2 * f_sense)  # 500 ns

# XY8 filter function peaks at f_sense and odd harmonics
freqs = np.linspace(0, 5e6, 1000)
filter_function = np.sinc(freqs * tau)**2  # Simplified

plt.figure(figsize=(10, 5))
plt.plot(freqs / 1e6, filter_function)
plt.xlabel("Frequency (MHz)")
plt.ylabel("Sensitivity")
plt.axvline(f_sense / 1e6, color='r', linestyle='--', label='Target frequency')
plt.title("XY8 Filter Function")
plt.legend()
plt.show()