Ramsey Spectroscopy and Atomic Clocks

Author

Daniel Fischer

Introduction

In this chapter, we apply the Bloch-vector formalism to one of its most important real-world uses: Ramsey interferometry, the core operating principle behind modern atomic clocks.

We focus on cesium–133, the atom that defines the SI second through the frequency of its hyperfine transition. By describing Ramsey spectroscopy using Bloch vectors, the underlying physics becomes intuitive and geometrically transparent.


1. Experimental Setup

Cesium is an alkali atom with electronic ground state configuration [Xe]\(6s\) and total angular momentum \(F=3\) or \(F=4\) due to hyperfine coupling with its nuclear spin \(I = 7/2\). The transition between these two hyperfine states has a frequency

\[ \omega_0 = 2\pi \times 9{,}192{,}631{,}770\ \mathrm{s^{-1}}, \]

a number so stable that it defines the second in the SI system.

Because the hyperfine states have an essentially infinite natural lifetime, damping in the Bloch equations can be neglected.

In a Ramsey experiment (Figure 1), an atomic beam passes through:

  1. First microwave interaction zone (first “\(\pi/2\) pulse”),
  2. Field-free drift region of duration \(T\),
  3. Second interaction zone (second “\(\pi/2\) pulse”),
  4. Detection region, where populations are measured.

This pulse–drift–pulse sequence creates quantum interference fringes that allow extremely precise determination of \(\omega_0\).

Two interaction zones separated by a drift region
Figure 1: Schematic of a Ramsey experiment.

2. Bloch Vector Dynamics in Ramsey Spectroscopy

The Bloch-vector description provides a simple geometric interpretation of the Ramsey sequence. We now walk through the evolution of the Bloch vector step by step.


2.1 Initial State

Atoms emerge from the source in the lower hyperfine level \(F=3\). In Bloch-vector representation this is the south pole of the Bloch sphere:

\[ \vec b_0 = \begin{pmatrix} 0 \\ 0 \\ -1 \end{pmatrix}. \]


2.2 Step 1 — First \(\pi/2\) pulse

In the first interaction zone, the microwave field is tuned such that the Rabi frequency satisfies \(\Omega_0 \tau = \pi/2\) and \(\Omega_0 \gg \delta\), so the Rabi vector points nearly along the \(u\)-axis.

Using the undamped Bloch equation
\[ \frac{d\vec b}{dt} = -\vec{\Omega} \times \vec b, \]
the Bloch vector rotates by \(\pi/2\) around the (negative) \(u\)-axis. Thus,

\[ \vec b_1 = \begin{pmatrix} 0 \\ -1 \\ 0 \end{pmatrix}. \]

The atom is now in an equal superposition of ground and excited states.

Code
import numpy as np
import matplotlib.pyplot as plt
import qutip as qt
from qutip import Bloch
from mpl_toolkits.mplot3d import Axes3D

# --- Set up plot styling ---
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.size'] = 14

fig = plt.figure(figsize=(3, 3))

ax = fig.add_subplot(111, projection='3d')


b = Bloch(axes=ax)
b.add_states([qt.basis(2, 1), 1.3* (1 * qt.basis(2, 0) + 1*qt.basis(2, 1)).unit(), (1 * qt.basis(2, 0) - 1j*qt.basis(2, 1)).unit()])
b.vector_color = ['#FF9999', 'b', 'r']
b.vector_width = 2

# Using QuTiP's built-in state evolution
# Rabi oscillation with detuning
H = -1*qt.sigmax() #- 1* qt.sigmaz()  # Hamiltonian (Pauli X)
psi0 = qt.basis(2, 1)  # Initial state |0⟩
times = np.linspace(0, np.pi/2, 15)
result = qt.sesolve(H.unit(), psi0, times)


for i in range(len(result.states)-2):
    b.add_arc(result.states[i], result.states[i+1], fmt="r", zorder=10, lw=1)
    #b.add_states(result.states[i], colors='r')

#result2=qt.sesolve(H.unit(), result.states[-2], [0,1.5*np.pi/14])
#b.add_arc(result.states[-2], result2.states[1], fmt="r", zorder=10, lw=1)
#b.add_states(result.states[-2], colors='r')


b.xlabel = ['','']
b.ylabel = [r'$v$','']
b.zlabel = [r'$w$','']
b.render()

new_arrowhead = qt.bloch.Arrow3D(xs=[-1, -1], ys=[0, 0], zs=[-0.1, 0],
                    mutation_scale=b.vector_mutation,
                    lw=1, arrowstyle=b.vector_style, color='r', zorder=15)
ax.add_artist(new_arrowhead)
ax.text(-.15,-1.1,0,r'$u$',ha='center', va='center', fontsize='20')
ax.text(-.2,-1.5,0,r'$\vec{\Omega}$',ha='center', va='center', fontsize='20')

plt.show()

Bloch-sphere diagram showing the effect of the first pi-over-two pulse. The initial Bloch vector points downward at the south pole. A rotation around the negative u-axis moves the vector into the equatorial plane, ending at the negative v direction. The diagram includes the Rabi vector, trajectory arcs, and labeled axes u, v, and w.


2.3 Step 2 — Field-free propagation

During the drift time \(T\), the atoms evolve freely at their eigenfrequency \(\omega_0\). The driving field in the two interaction zones has frequency \(\omega_L\), so the two accumulate a relative phase at the detuning

\[ \delta = \omega_L - \omega_0. \]

The Bloch vector now rotates around the (negative) \(w\)-axis with angular velocity \(\delta\):

\[ \vec b_1 \xrightarrow{\text{drift}} \vec b(T) = \begin{pmatrix} -\sin(\delta T) \\[4pt] -\cos(\delta T) \\[4pt] 0 \end{pmatrix}. \]

Special cases:

  • Case 1: \(\delta T = 2\pi n\)
    The vector returns to
    \[ \vec b_1 = (0,\,-1,\,0)^\top. \] In this situation, the field and the atomic dipole are in phase after an integer number of full precession cycles. The Bloch vector therefore returns to its original azimuthal angle in the equatorial plane.

  • Case 2: \(\delta T = (2n+1)\pi\)
    The vector becomes
    \[ \vec b_2 = (0,\,+1,\,0)^\top. \] Here, the field and the atomic dipole are out of phase by \(\pi\) after the evolution time. As a result, the Bloch vector points to the opposite azimuthal direction in the equatorial plane.

Any other detuning produces an intermediate azimuthal angle in the equatorial plane.

Code
import numpy as np
import matplotlib.pyplot as plt
import qutip as qt
from qutip import Bloch
from mpl_toolkits.mplot3d import Axes3D

# --- Set up plot styling ---
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.size'] = 14

fig = plt.figure(figsize=(7, 4))

ax = fig.add_subplot(121, projection='3d')


b = Bloch(axes=ax)
b.add_states([0.6* qt.basis(2, 0), (1 * qt.basis(2, 0) - 1j*qt.basis(2, 1)).unit()])
b.vector_color = ['b', 'r']
b.vector_width = 2

# Using QuTiP's built-in state evolution
# Rabi oscillation with detuning
H = - 1* qt.sigmaz()  # Hamiltonian (Pauli X)
psi0 = (1 * qt.basis(2, 0) - 1j*qt.basis(2, 1)).unit()  # Initial state |0⟩
times = np.linspace(0, np.pi*2, 15)
result = qt.sesolve(H.unit(), psi0, times)


for i in range(len(result.states)-2):
    b.add_arc(result.states[i], result.states[i+1], fmt="r", zorder=10, lw=1)
    #b.add_states(result.states[i], colors='r')

result2=qt.sesolve(H.unit(), result.states[-2], [0,1.5*np.pi/14])
b.add_arc(result.states[-2], result2.states[1], fmt="r", zorder=10, lw=1)
#b.add_states(result.states[-2], colors='r')


b.xlabel = [r'$u$','']
b.ylabel = [r'$v$','']
b.zlabel = [r'$w$','']
b.render()

new_arrowhead = qt.bloch.Arrow3D(xs=[-0.99, -1], ys=[-0.1, 0], zs=[0, 0],
                    mutation_scale=b.vector_mutation,
                    lw=1, arrowstyle=b.vector_style, color='r', zorder=15)
ax.add_artist(new_arrowhead)
#ax.text(-.1,-1.1,0,r'$u$',ha='center', va='center', fontsize='20')
ax.text(.2,0,0.2,r'$\vec{\Omega}$',ha='center', va='center', fontsize='20')
ax.set_title(r'Case $1$:')

ax2 = fig.add_subplot(122, projection='3d')

b2 = Bloch(axes=ax2)
b2.add_states([(1 * qt.basis(2, 0) - 1j*qt.basis(2, 1)).unit(), 0.5* qt.basis(2, 0), (1 * qt.basis(2, 0) + 1j*qt.basis(2, 1)).unit()])
b2.vector_color = ['#FF9999','b', 'r']
b2.vector_width = 2


times2 = np.linspace(0, np.pi, 8)
result3 = qt.sesolve(H.unit(), psi0, times2)


for i in range(len(result3.states)-2):
    b2.add_arc(result3.states[i], result3.states[i+1], fmt="r", zorder=10, lw=1)
    #b.add_states(result.states[i], colors='r')

result4=qt.sesolve(H.unit(), result3.states[-2], [0,1.5*np.pi/14])
b2.add_arc(result3.states[-2], result4.states[1], fmt="r", zorder=10, lw=1)
#b.add_states(result.states[-2], colors='r')


b2.xlabel = [r'$u$','']
b2.ylabel = [r'$v$','']
b2.zlabel = [r'$w$','']
b2.render()

new_arrowhead2 = qt.bloch.Arrow3D(xs=[0.99, 1], ys=[0.1, 0], zs=[0, 0],
                    mutation_scale=b.vector_mutation,
                    lw=1, arrowstyle=b.vector_style, color='r', zorder=15)
ax2.add_artist(new_arrowhead2)

ax2.text(.2,0,0.2,r'$\vec{\Omega}$',ha='center', va='center', fontsize='20')

ax2.set_title(r'Case $2$:')

plt.show()

Two Bloch-sphere panels illustrating free precession during the Ramsey drift time. Left panel: Case 1, where delta T equals an even multiple of pi, showing the Bloch vector returning to the negative v direction, corresponding to in-phase field and dipole. Right panel: Case 2, where delta T equals an odd multiple of pi, showing the Bloch vector rotating to the positive v direction, corresponding to an out-of-phase evolution. Both panels include trajectory arcs, Rabi vectors, and labeled axes.


2.4 Step 3 — Second \(\pi/2\) pulse

In the second interaction zone, the same \(\pi/2\) pulse rotates the Bloch vector by \(-\pi/2\) around the \(u\)-axis.

  • Case 1 (\(\delta T = 2\pi n\)):

    \[ (0,\,-1,\,0)^\top \ \xrightarrow{\ -\pi/2\ } \ (0,\,0,\,1)^\top, \]

    meaning complete transfer to the excited state.

  • Case 2 (\(\delta T=(2n+1)\pi\)):

    \[ (0,\,+1,\,0)^\top \ \xrightarrow{\ -\pi/2\ } \ (0,\,0,\,-1)^\top, \]

    meaning return to the ground state.

Intermediate angles lead to partial population transfer, generating the Ramsey interference pattern.

Code
import numpy as np
import matplotlib.pyplot as plt
import qutip as qt
from qutip import Bloch
from mpl_toolkits.mplot3d import Axes3D

# --- Set up plot styling ---
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.size'] = 14

fig = plt.figure(figsize=(7, 4))

ax = fig.add_subplot(121, projection='3d')


b = Bloch(axes=ax)
b.add_states([qt.basis(2, 0), 1.3* (1 * qt.basis(2, 0) + 1*qt.basis(2, 1)).unit(), (1 * qt.basis(2, 0) - 1j*qt.basis(2, 1)).unit()])
b.vector_color = ['r','b', '#FF9999']
b.vector_width = 2

# Using QuTiP's built-in state evolution
# Rabi oscillation with detuning
H = - 1* qt.sigmax()  # Hamiltonian (Pauli X)
psi0 = (1 * qt.basis(2, 0) - 1j*qt.basis(2, 1)).unit()  # Initial state |0⟩
times = np.linspace(0, np.pi/2, 15)
result = qt.sesolve(H.unit(), psi0, times)


for i in range(len(result.states)-2):
    b.add_arc(result.states[i], result.states[i+1], fmt="r", zorder=10, lw=1)
    #b.add_states(result.states[i], colors='r')

#result2=qt.sesolve(H.unit(), result.states[-2], [0,1.5*np.pi/14])
#b.add_arc(result.states[-2], result2.states[1], fmt="r", zorder=10, lw=1)
#b.add_states(result.states[-2], colors='r')


b.xlabel = ['','']
b.ylabel = [r'$v$','']
b.zlabel = [r'$w$','']
b.render()

new_arrowhead = qt.bloch.Arrow3D(xs=[-0.1, 0], ys=[0, 0], zs=[0.99, 1],
                    mutation_scale=b.vector_mutation,
                    lw=1, arrowstyle=b.vector_style, color='r', zorder=15)
ax.add_artist(new_arrowhead)
ax.text(-.15,-1.1,0,r'$u$',ha='center', va='center', fontsize='20')
ax.text(-.2,-1.5,0,r'$\vec{\Omega}$',ha='center', va='center', fontsize='20')
ax.set_title(r'Case $1$:')




ax2 = fig.add_subplot(122, projection='3d')

b2 = Bloch(axes=ax2)
b2.add_states([qt.basis(2, 1), 1.3* (1 * qt.basis(2, 0) + 1*qt.basis(2, 1)).unit(), (1 * qt.basis(2, 0) + 1j*qt.basis(2, 1)).unit()])
b2.vector_color = ['r','b', '#FF9999']
b2.vector_width = 2


times2 = np.linspace(0, np.pi/2, 15)
result3 = qt.sesolve(H.unit(), (1 * qt.basis(2, 0) + 1j*qt.basis(2, 1)).unit(), times2)


for i in range(len(result3.states)-2):
    b2.add_arc(result3.states[i], result3.states[i+1], fmt="r", zorder=10, lw=1)


b2.xlabel = ['','']
b2.ylabel = [r'$v$','']
b2.zlabel = [r'$w$','']
b2.render()

new_arrowhead2 = qt.bloch.Arrow3D(xs=[0.1, 0], ys=[0, 0], zs=[-0.99, -1],
                    mutation_scale=b.vector_mutation,
                    lw=1, arrowstyle=b.vector_style, color='r', zorder=15)
ax2.add_artist(new_arrowhead2)

ax2.text(-.15,-1.1,0,r'$u$',ha='center', va='center', fontsize='20')
ax2.text(-.2,-1.5,0,r'$\vec{\Omega}$',ha='center', va='center', fontsize='20')

ax2.set_title(r'Case $2$:')

plt.show()

Two Bloch-sphere panels showing the action of the second pi-over-two pulse in the Ramsey sequence. Left panel: Case 1, where the Bloch vector rotates from the negative v direction to the north pole, representing full excitation. Right panel: Case 2, where the Bloch vector rotates from the positive v direction back to the south pole, representing return to the ground state. Each panel shows vector trajectories, rotation axes, and the labeled u, v, and w axes.


3. Ramsey Spectrum and Interpretation

The excited-state population after the second pulse oscillates as a function of detuning:

\[ P_e(\delta) = \cos^2\!\left(\frac{\delta T}{2}\right). \]

This produces the characteristic Ramsey fringes, shown in Figure 2.

Code
import numpy as np
import matplotlib.pyplot as plt
import qutip as qt

# --- Set up plot styling ---
plt.rcParams['text.usetex'] = True
plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.size'] = 14

# Define projection operators
Pg = qt.basis(2, 0) * qt.basis(2, 0).dag()  # Ground state |g⟩⟨g|
Pe = qt.basis(2, 1) * qt.basis(2, 1).dag()  # Excited state |e⟩⟨e|

def propagate_state(state_in, Omega0, delta, t):
    """
    Single pulse evolution (for comparison with Ramsey)
    """
    H = -delta/2 * qt.sigmaz() - Omega0/2 * qt.sigmax()
    
    # Time evolution with expectation value
    result = qt.sesolve(H, state_in, [0, t], e_ops=[Pe])
       
    return result.expect[0][-1]


def propagate_state_Ramsey(state_in, Omega0, delta, t_pulse, tau):
    """
    Ramsey spectroscopy sequence:
    1. Apply π/2 pulse (creates superposition)
    2. Free evolution for time tau (accumulates phase)
    3. Apply second π/2 pulse (converts phase to population)
    """
    # Hamiltonians
    H_off = -delta/2 * qt.sigmaz()  # Free evolution (laser off)
    H_on = H_off - Omega0/2 * qt.sigmax()  # Driven evolution (laser on)
    
    # Step 1: First π/2 pulse
    state1 = qt.sesolve(H_on, state_in, [0, t_pulse]).states[-1]
    
    # Step 2: Free evolution for time tau
    state2 = qt.sesolve(H_off, state1, [0, tau]).states[-1]
    
    # Step 3: Second π/2 pulse
    state3 = qt.sesolve(H_on, state2, [0, t_pulse]).states[-1]
    
    # Measure excited state population
    population = qt.expect(Pe, state3)
       
    return population


# Initial state: ground state |g⟩
psi0 = qt.basis(2, 0)  # |g⟩ = |0⟩

# Parameters
Omega0 = 1.0  # Rabi frequency
t_pi_half = np.pi / 2  # π/2 pulse duration at resonance

# Scan over detuning
delta_values = np.linspace(-10, 10, 1000)

# Calculate populations for different free evolution times
tau_values = [10*np.pi]   # e.g., long drift time

# Create figure
fig, ax = plt.subplots(figsize=(7.5, 5))

# Single π/2 pulse
populations_single = []
for delta in delta_values:
    pop = propagate_state(psi0, Omega0, delta, t_pi_half)
    populations_single.append(pop)
populations_single = np.array(populations_single)

# Ramsey
colors = ['blue']
for tau, color in zip(tau_values, colors):
    populations_ramsey = []
    for delta in delta_values:
        pop = propagate_state_Ramsey(psi0, Omega0, delta, t_pi_half, tau)
        populations_ramsey.append(pop)
    populations_ramsey = np.array(populations_ramsey)
    
    ax.plot(delta_values, populations_ramsey, color=color, linewidth=1, 
             label=f'$T = 20\\tau$')

ax.plot(delta_values, populations_single, 'k-', linewidth=2, label='Single $\\pi/2$ pulse')

ax.axvline(x=0, color='r', linestyle='--', alpha=0.5, label='Resonance')
ax.set_xlabel(r'Detuning $\delta$ (units of $\Omega_0$)', fontsize=14)
ax.set_ylabel(r'Excited State Population', fontsize=14)
ax.grid(True, alpha=0.3)
ax.legend()
ax.set_ylim(0, 1.05)
ax.set_xlim(-10, 10)

plt.tight_layout()
plt.show()
Excited-state population vs detuning showing narrow Ramsey pattern.
Figure 2: Ramsey fringes compared with single-pulse excitation.

3.1 What is actually happening physically?

Traditional laser spectroscopy measures how strongly a monochromatic field can drive transitions between two states. In that case, the width of the excitation line is fundamentally limited by the interaction time: a short pulse produces a broad line (Fourier limit), and only by making the interaction longer can the resonance be determined more precisely.

Ramsey spectroscopy works differently. Instead of driving the atom continuously, we apply two short \(\pi/2\) pulses separated by a long “free evolution” time \(T\). The key idea is:

Between the pulses, the atom and the field evolve independently. After the second pulse, we compare how much phase each has accumulated.

If the laser frequency matches the atomic transition exactly, the relative phase stays constant, and the second pulse always produces the same final population. If there is a small detuning \(\delta\), however, the atomic dipole accumulates phase at a slightly different rate than the laser. After time \(T\), the phase mismatch is

\[ \Delta\varphi = \delta T, \]

and the second \(\pi/2\) pulse converts this accumulated phase difference into a measurable change in population. When plotted versus \(\delta\), this produces the narrow Ramsey fringe pattern.

Why are Ramsey fringes so narrow?

Because the “frequency measurement” is based on how much phase difference builds up during the long time \(T\) — not on how strongly the atom is driven. This means:

  • The longer you wait, the more phase accumulates,
    → the fringes get narrower,
    → the frequency resolution improves.

In fact, the fringe spacing is approximately

\[ \Delta\delta \sim \frac{2\pi}{T}. \]

So the resolution is directly set by how long the atom is allowed to precess freely.


3.2 Comparison with traditional single-pulse (Rabi) spectroscopy

Rabi (continuous-wave or long-pulse) spectroscopy:

  • Resolution is limited by the Fourier width of the pulse.
  • To improve resolution, you must increase pulse duration.
  • Increasing the duration requires much more stable intensity and phase.
  • The atom is always being driven → the drive itself broadens the line.

Ramsey spectroscopy:

  • The two pulses can be very short — their only job is to prepare and read out the superposition.
  • The long drift time \(T\) determines the resolution, not the pulse length.
  • During the free evolution, the atom is undisturbed, so the interference pattern reflects the true phase accumulation with minimal broadening.
  • Imperfections in the drive field mostly reduce contrast but do not shift the fringe positions.

This makes Ramsey’s method fundamentally better for high-precision measurements.


3.3 Why this matters

Ramsey interferometry provides:

  • Extremely sharp spectral features, vastly narrower than Rabi lines.
  • High immunity to drive-field imperfections (only the contrast suffers).
  • Direct phase-comparison measurement, which is much more sensitive than amplitude-based excitation measurement.
  • A strategy for achieving arbitrarily high resolution simply by increasing the free-evolution time.

These properties are why the Ramsey method underlies modern atomic clocks, molecular beam resonance experiments, and nearly all precision measurements of fundamental constants.

How this leads to an actual clock

To see how one builds a clock from such a spectroscopy experiment, it helps to recall that the goal of any atomic clock is to stabilize a local oscillator—typically a microwave or optical field—so that its frequency matches the natural transition frequency of the atom. The Ramsey sequence provides the crucial error signal needed for this stabilization.

The basic idea is:

  1. Run the Ramsey sequence repeatedly while scanning or dithering the oscillator frequency slightly around a chosen value.
  2. Measure the population in the excited state (or equivalently the position of the Bloch vector) after each cycle.
  3. Because the Ramsey fringe pattern has a very steep slope near its central fringe, small changes in frequency produce measurable changes in excitation probability.
  4. The sign of this change tells you whether the oscillator is above or below the atomic resonance.
  5. Feeding this information back into a servo loop allows the oscillator frequency to be continuously adjusted so that it stays locked to the atomic transition.

In this way, the atoms act as a phase reference: the accumulated phase during the free-evolution time compares the oscillator’s frequency to the atomic frequency. Any mismatch produces a measurable phase shift, and the servo system corrects it. When locked, the oscillator inherits the stability and accuracy of the atomic transition, forming the basis of an atomic clock.


4. Further Developments

Modern atomic clocks build on the Ramsey principle but incorporate additional innovations:

  • Cesium fountain clocks (e.g., NIST-F2)
    Laser-cooled cesium atoms are launched upward, pass twice through a microwave cavity, and experience two \(\pi/2\) pulses in free fall. Cooling dramatically reduces velocity spread, enabling uncertainties at the level of 1 second per 300 million years.

  • Optical clocks
    Replace microwave transitions with optical ones (\(\sim 10^{14}\)\(10^{15}\,\mathrm{Hz}\)). Higher frequency → proportionally higher precision. State-of-the-art optical clocks (Yb, Sr, Al\(^+\)) now surpass cesium fountains in stability and accuracy.

  • Advanced Ramsey schemes
    Composite pulses, phase-modulated interrogation, and spin-echo–like techniques further suppress systematic shifts.


Key Takeaways

  • Ramsey spectroscopy uses two \(\pi/2\) pulses separated by free evolution to create quantum interference sensitive to detuning.
  • The Bloch-vector description makes the dynamics intuitive: successive rotations around the \(u\)- and \(w\)-axes map directly onto population outcomes.
  • The excited-state probability oscillates as
    \[P_e(\delta) = \cos^2\!\left(\frac{\delta T}{2}\right),\]
    producing narrow fringes ideal for precision metrology.
  • The exceptional accuracy of modern atomic clocks stems from long coherence times, reduced systematic effects, and the ability to increase the free-evolution time.
  • Ramsey interferometry remains a central technique in frequency metrology and underlies both microwave and optical atomic clocks.