import math
import numpy as np
import plotly.graph_objects as go
from clerq import mb_solve, plot
from clerq.plot import theme # register Plotly templateTwo-Level: Stimulated Three-Pulse Photon Echo
Three short π/2 pulses separated by delays τ and \(T_w\) generate a stimulated photon echo at time \(t = +\tau\) after the third pulse. The defining feature compared to the two-pulse echo (A4) is that coherence information is stored as a population grating during \(T_w\), so the echo amplitude decays with the population lifetime \(T_1\) rather than the coherence lifetime \(T_2\):
\[ A_{\rm echo}(\tau, T_w) \propto e^{-2\tau/T_2}\, e^{-T_w/T_1} \]
A \(T_w\) sweep isolates \(T_1\); a τ sweep (already covered in the photon-echo example) isolates \(T_2\). The stimulated echo is the optical analogue of the NMR stimulated echo (Hahn 1950; Mossberg 1982).
Physical mechanism
π/2 pulse at \(t_1 = -(\tau + T_w)\): creates a ground/excited superposition. Each velocity class begins precessing at its own Doppler- shifted frequency.
Free precession over τ: the macroscopic polarisation \(P \propto \sum_j \rho_{10}^{(j)}\) dephases on the Doppler timescale \(1/\sigma\).
π/2 pulse at \(t_2 = -T_w\): maps the surviving coherences onto a sinusoidal population grating in velocity space. The net coherence vanishes (destructive interference among velocity classes), but the population difference carries the phase record.
Storage interval \(T_w\): the population grating persists. Its amplitude decays as \(e^{-T_w/T_1}\) (spontaneous emission). There is no macroscopic coherence here — this is the defining distinction from the two-pulse echo.
π/2 pulse at \(t_3 = 0\): reads the population grating back into coherences with the same phase record as step 2.
Rephasing over τ: the restored coherences rephase — echo at \(t = +\tau\).
Hard-pulse condition
All three pulses must be hard (spectrally broad enough to rotate every velocity class by exactly π/2):
\[\Omega_{\rm peak} \gg \Delta_{\rm Doppler}\]
Here \(T = 0.025\,\gamma^{-1}\) gives \(\Omega_{\rm peak} = 10\,{\rm rad}\,\gamma\) and \(\Omega_{\rm peak}/\sigma \approx 2.4\), satisfying the condition for all significantly-weighted velocity classes.
Level structure
|1⟩ ════════════════ (excited)
│ decay rate 0.1 → T₁ ≈ 1.59 γ⁻¹, T₂ ≈ 3.18 γ⁻¹
Ω │ three π/2 sech pulses (hard-pulse limit)
│
|0⟩ ════════════════ (ground)
Inhomogeneous broadening: Gaussian Doppler distribution, \(\sigma \approx 4.2\,{\rm rad}\,\gamma\) (thermal_width = 0.15).
Parameters
| Quantity | Value | Notes |
|---|---|---|
Decay rate |
\(0.1\) | \(\Gamma = 2\pi\times 0.1 \approx 0.63\,{\rm rad}\,\gamma\); \(T_1 \approx 1.59\), \(T_2 \approx 3.18\,\gamma^{-1}\) |
| Doppler σ | \(4.2\,{\rm rad}\,\gamma\) | 40 velocity classes over \(\pm 0.75\) |
| Pulse width | \(T = 0.025\,\gamma^{-1}\) | hard-pulse: \(\Omega_{\rm peak} = 10\,{\rm rad}\,\gamma\) |
| Inter-pulse delay | \(\tau = 0.5\,\gamma^{-1}\) | echo at \(t = +0.5\,\gamma^{-1}\) |
| Storage interval | \(T_w = 1.0\,\gamma^{-1}\) (main) | \(e^{-T_w/T_1} \approx 53\%\) amplitude |
# interaction_strengths = 0.5: clerq 0.13.0 corrected a factor of 2 in the Maxwell
# propagation equation, so 0.5 gives the optical depth that 1.0 gave in earlier versions.
tau = 0.5 # delay between pulse 1–2 and rephasing time after pulse 3
Tw = 1.0 # storage interval (between pulse 2 and pulse 3)
T = 0.025 # sech half-width — short hard pulses, Ω_peak >> σ
# Pulse centres:
# pulse 1 (π/2) at t₁ = −(τ + Tw)
# pulse 2 (π/2) at t₂ = −Tw
# pulse 3 (π/2) at t₃ = 0
# echo at t_echo = +τ
t1 = -(tau + Tw) # = −1.5
t2 = -Tw # = −1.0
t3 = 0.0
mb_solve_json = f"""
{{
"atom": {{
"num_states": 2,
"decays": [
{{"channels": [[0, 1]], "rate": 0.1}}
],
"fields": [
{{
"label": "p1",
"coupled_levels": [[0, 1]],
"rabi_freq": 1.0,
"rabi_freq_t_func": "sech",
"rabi_freq_t_args": {{"n_pi": 0.5, "centre": {t1}, "width": {T}}}
}},
{{
"label": "p2",
"coupled_levels": [[0, 1]],
"rabi_freq": 1.0,
"rabi_freq_t_func": "sech",
"rabi_freq_t_args": {{"n_pi": 0.5, "centre": {t2}, "width": {T}}}
}},
{{
"label": "p3",
"coupled_levels": [[0, 1]],
"rabi_freq": 1.0,
"rabi_freq_t_func": "sech",
"rabi_freq_t_args": {{"n_pi": 0.5, "centre": {t3}, "width": {T}}}
}}
]
}},
"t_min": {-(tau + Tw + 1.0)},
"t_max": {tau + 0.5},
"t_steps": 1000,
"z_min": 0.0,
"z_max": 1.0,
"z_steps": 20,
"z_steps_inner": 2,
"interaction_strengths": [0.5, 0.5, 0.5],
"velocity_classes": {{
"thermal_width": 0.15,
"thermal_delta_min": -0.75,
"thermal_delta_max": 0.75,
"thermal_delta_steps": 40
}},
"savefile": "mbs-two-stimulated-echo"
}}
"""
mbs = mb_solve.MBSolve().from_json_str(mb_solve_json)
mbs.mbsolve(recalc=False)
print("Done")Saving MBSolve to mbs-two-stimulated-echo.qu
Done
Output field: stimulated echo at \(t = +\tau\)
The total transmitted field \(|\Omega_{p1}(z_{\rm max},t)| + |\Omega_{p2}(z_{\rm max},t)| + |\Omega_{p3}(z_{\rm max},t)|\) shows three attenuated input pulses and the stimulated echo at \(t = +\tau\). All three pulses drive the same transition so they all experience Beer-Lambert absorption at their respective times.
Vertical dashed lines mark the three pulse centres (\(t_1\), \(t_2\), \(t_3\)) and the echo time \(t_{\rm echo} = +\tau\).
t = mbs.tlist
Omega_in = (np.abs(mbs.Omegas_zt[0, 0, :]) +
np.abs(mbs.Omegas_zt[1, 0, :]) +
np.abs(mbs.Omegas_zt[2, 0, :]))
Omega_out = (np.abs(mbs.Omegas_zt[0, -1, :]) +
np.abs(mbs.Omegas_zt[1, -1, :]) +
np.abs(mbs.Omegas_zt[2, -1, :]))
fig = go.Figure(layout=go.Layout(
title="Stimulated photon echo: transmitted field at z = z_max",
xaxis_title="time (γ⁻¹)",
yaxis_title="|Ω| (γ)",
template="clerq",
))
fig.add_trace(go.Scatter(x=t, y=Omega_in, name="input (z = 0)", line=dict(dash="dot")))
fig.add_trace(go.Scatter(x=t, y=Omega_out, name="output (z = z_max)"))
fig.add_vline(x=t1, line_dash="dash", line_color="grey",
annotation_text="π/2 (p1)", annotation_position="top right")
fig.add_vline(x=t2, line_dash="dash", line_color="grey",
annotation_text="π/2 (p2)", annotation_position="top right")
fig.add_vline(x=t3, line_dash="dash", line_color="grey",
annotation_text="π/2 (p3)", annotation_position="top right")
fig.add_vline(x=tau, line_dash="dash", line_color="red",
annotation_text="echo", annotation_position="top right")
fig.show(renderer='notebook_connected')Macroscopic polarisation: FID, collapse, and revival
\(\mathrm{Im}[\rho_{10}(z=0,t)]\) is the velocity-class-averaged coherence. Three stages are visible:
- FID after pulse 1: the polarisation builds up then dephases on the Doppler timescale \(1/\sigma \approx 0.24\,\gamma^{-1}\).
- Population-grating interval (\(T_w\), between pulse 2 and pulse 3): the net coherence collapses to near zero. The phase information is stored in population, not coherence — no FID is detectable here.
- Echo at \(t = +\tau\): pulse 3 reads the population grating back into coherences, which rephase to produce the echo.
fig = plot.coherence(mbs, i=1, j=0, z_idx=0, component="imag")
fig.update_layout(
title="Im[ρ₁₀(t)] at z = 0 — FID, population-grating storage, echo"
)
fig.add_vline(x=t1, line_dash="dash", line_color="grey",
annotation_text="π/2 (p1)", annotation_position="top right")
fig.add_vline(x=t2, line_dash="dash", line_color="grey",
annotation_text="π/2 (p2)", annotation_position="top right")
fig.add_vline(x=t3, line_dash="dash", line_color="grey",
annotation_text="π/2 (p3)", annotation_position="top right")
fig.add_vline(x=tau, line_dash="dash", line_color="red",
annotation_text="echo", annotation_position="top right")
# Shade the storage interval T_w
fig.add_vrect(x0=t2, x1=t3, fillcolor="grey", opacity=0.08,
layer="below", line_width=0,
annotation_text="T_w (population storage)",
annotation_position="top left")
fig.show(renderer='notebook_connected')Coherence during \(T_w\)
The “zero coherence during \(T_w\)” picture has two caveats worth noting:
- FID from pulse 2 itself dephases on the same Doppler timescale \(1/\sigma \approx 0.24\,\gamma^{-1}\).
- Secondary two-pulse echo from pulses 1 and 2 — a pair of \(\pi/2\) pulses does generate a partial photon echo, peaking at \(t_2 + \tau = -T_w + \tau\). With \(T_w = 1.0\) and \(\tau = 0.5\) this secondary echo sits at \(t = -0.5\,\gamma^{-1}\), only \(0.5\,\gamma^{-1}\) before \(t_3\), so its dephasing tail overlaps the late storage window. For large \(T_w \gg \tau\) this echo has time to fully dephase.
The cell below measures the maximum net coherence in the late storage window (after the FID from pulse 2 has died) and at the echo time.
rho10 = mbs.states_zt[0, :, 1, 0] # at z = 0
t_arr = mbs.tlist
# Late storage window — after FID from pulse 2 but may overlap secondary echo
# secondary echo from pulses 1+2 is at t2 + tau = -T_w + tau = -0.5
storage_mask = (t_arr > t2 + 0.7) & (t_arr < t3 - 0.05)
rho_storage_max = float(np.abs(rho10[storage_mask]).max())
# Echo peak |ρ₁₀| near t = +τ
echo_mask = (t_arr > tau - 5*T) & (t_arr < tau + 5*T)
rho_echo_max = float(np.abs(rho10[echo_mask]).max())
print(f"|ρ₁₀| max in late T_w window [t₂+0.7, t₃−0.05] : {rho_storage_max:.4f}")
print(f"|ρ₁₀| max at echo (t ≈ +τ) : {rho_echo_max:.4f}")
print(f"Suppression ratio (storage / echo) : {rho_storage_max/rho_echo_max:.3f}")
# The secondary two-pulse echo from pulses 1+2 overlaps this window at T_w=1.0;
# for T_w >> tau the secondary echo fully dephases before t3 and this value drops.
print("(The residual coherence at T_w=1.0 is the tail of the secondary echo")
print(" from pulses 1+2, which peaks at t2+tau=-0.5 and dephases over ~0.5 γ⁻¹.)")|ρ₁₀| max in late T_w window [t₂+0.7, t₃−0.05] : 0.1841
|ρ₁₀| max at echo (t ≈ +τ) : 0.0716
Suppression ratio (storage / echo) : 2.570
(The residual coherence at T_w=1.0 is the tail of the secondary echo
from pulses 1+2, which peaks at t2+tau=-0.5 and dephases over ~0.5 γ⁻¹.)
Echo amplitude vs \(T_w\): \(T_1\) measurement
Running the simulation for storage intervals \(T_w \in [1.0, 2.5]\,\gamma^{-1}\) at fixed \(\tau = 0.5\,\gamma^{-1}\) demonstrates the exponential decay:
\[ A_{\rm echo}(T_w) \propto e^{-T_w/T_1}, \qquad T_1 = \frac{1}{\Gamma} = \frac{1}{2\pi\times {\rm rate}} \approx 1.59\,\gamma^{-1} \]
The range \(T_w \ge 1.0\,\gamma^{-1}\) is used because shorter \(T_w\) values are contaminated by the Hahn echo from pulse-pair (2,3), which appears at \(t = T_w\) and falls inside the measurement window when \(T_w \le 0.75\,\gamma^{-1}\).
Echo amplitude is measured as the peak of the transmitted field \(\sum_i |\Omega_i(z_{\rm max},t)|\) in the window \(t\in[0.2,\,0.75]\,\gamma^{-1}\) (after the three input pulses have passed, before the (2,3) echo at \(t=T_w\)).
rate = 0.1
T1 = 1.0 / (2 * math.pi * rate) # ≈ 1.592 γ⁻¹
T2 = 2.0 / (2 * math.pi * rate) # ≈ 3.183 γ⁻¹
# T_w ≥ 1.0: the Hahn echo from pulses (2,3) at t = T_w is outside
# the measurement window [0.2, 0.75], leaving only the stimulated echo.
Tw_values = [1.0, 1.5, 2.0, 2.5]
echo_amps = []
for Tw_val in Tw_values:
t1_v = -(tau + Tw_val)
t2_v = -Tw_val
t_min_v = -(tau + Tw_val + 1.0)
t_max_v = tau + 0.5
# Maintain dt ≤ 0.004 so hard pulses (T = 0.025) remain well resolved
t_steps_v = max(500, int((t_max_v - t_min_v) / 0.004))
j = f"""
{{
\"atom\": {{
\"num_states\": 2,
\"decays\": [{{\"channels\": [[0, 1]], \"rate\": {rate}}}],
\"fields\": [
{{\"label\":\"p1\",\"coupled_levels\":[[0,1]],\"rabi_freq\":1.0,
\"rabi_freq_t_func\":\"sech\",
\"rabi_freq_t_args\":{{\"n_pi\":0.5,\"centre\":{t1_v},\"width\":{T}}}}},
{{\"label\":\"p2\",\"coupled_levels\":[[0,1]],\"rabi_freq\":1.0,
\"rabi_freq_t_func\":\"sech\",
\"rabi_freq_t_args\":{{\"n_pi\":0.5,\"centre\":{t2_v},\"width\":{T}}}}},
{{\"label\":\"p3\",\"coupled_levels\":[[0,1]],\"rabi_freq\":1.0,
\"rabi_freq_t_func\":\"sech\",
\"rabi_freq_t_args\":{{\"n_pi\":0.5,\"centre\":0.0,\"width\":{T}}}}}
]
}},
\"t_min\": {t_min_v},
\"t_max\": {t_max_v},
\"t_steps\": {t_steps_v},
\"z_min\": 0.0, \"z_max\": 1.0, \"z_steps\": 20, \"z_steps_inner\": 2,
\"interaction_strengths\": [0.5, 0.5, 0.5],
\"velocity_classes\": {{
\"thermal_width\": 0.15,
\"thermal_delta_min\": -0.75,
\"thermal_delta_max\": 0.75,
\"thermal_delta_steps\": 40
}},
\"savefile\": \"mbs-two-stimulated-echo-Tw{Tw_val:.2f}\"
}}
"""
m = mb_solve.MBSolve().from_json_str(j)
m.mbsolve(recalc=False)
t_v = m.tlist
# Peak of total output field in the echo window.
# Window [0.2, 0.75]: after input pulses (last at t=0) and before
# the (2,3) Hahn echo at t = T_w ≥ 1.0.
Omega_out = sum(np.abs(m.Omegas_zt[i, -1, :]) for i in range(3))
echo_mask = (t_v >= 0.2) & (t_v <= 0.75)
amp = float(Omega_out[echo_mask].max()) if echo_mask.any() else 0.0
echo_amps.append(amp)
print(f"T_w = {Tw_val:.1f} γ⁻¹ — echo peak |Ω_out|: {amp:.4f}")Saving MBSolve to mbs-two-stimulated-echo-Tw1.00.qu
T_w = 1.0 γ⁻¹ — echo peak |Ω_out|: 1.5223
Saving MBSolve to mbs-two-stimulated-echo-Tw1.50.qu
T_w = 1.5 γ⁻¹ — echo peak |Ω_out|: 1.0720
Saving MBSolve to mbs-two-stimulated-echo-Tw2.00.qu
T_w = 2.0 γ⁻¹ — echo peak |Ω_out|: 0.8760
Saving MBSolve to mbs-two-stimulated-echo-Tw2.50.qu
T_w = 2.5 γ⁻¹ — echo peak |Ω_out|: 0.6413
Tw_arr = np.array(Tw_values)
amps = np.array(echo_amps)
# Fit normalised to T_w = 1.0 (first data point)
Tw_fine = np.linspace(Tw_arr[0], Tw_arr[-1] + 0.5, 200)
analytic = amps[0] * np.exp(-(Tw_fine - Tw_arr[0]) / T1)
fig = go.Figure(layout=go.Layout(
title=f"Stimulated echo amplitude vs T_w — T₁ = {T1:.2f} γ⁻¹",
xaxis_title="T_w (γ⁻¹)",
yaxis_title="peak |Ω(z_max, t)| in echo window",
template="clerq",
))
fig.add_trace(go.Scatter(
x=Tw_arr, y=amps, mode="markers+lines", name="simulation",
marker=dict(size=8)
))
fig.add_trace(go.Scatter(
x=Tw_fine, y=analytic, mode="lines",
name=f"e^(−ΔT_w/T₁), T₁ = {T1:.2f} γ⁻¹",
line=dict(dash="dash")
))
fig.show(renderer='notebook_connected')
# Print deviations from analytic
print("T_w (γ⁻¹) | measured | analytic | deviation")
for tw, amp in zip(Tw_arr, amps):
expected = amps[0] * np.exp(-(tw - Tw_arr[0]) / T1)
print(f" {tw:.1f} | {amp:.4f} | {expected:.4f} | {(amp-expected)/expected*100:+.1f}%")T_w (γ⁻¹) | measured | analytic | deviation
1.0 | 1.5223 | 1.5223 | +0.0%
1.5 | 1.0720 | 1.1119 | -3.6%
2.0 | 0.8760 | 0.8121 | +7.9%
2.5 | 0.6413 | 0.5932 | +8.1%
Pulse-area sanity check
Each of the three input fields should carry a π/2 pulse area at \(z = 0\).
for i, label in enumerate(["p1 (π/2)", "p2 (π/2)", "p3 (π/2)"]):
area = np.trapezoid(mbs.Omegas_zt[i, 0, :].real, mbs.tlist) / np.pi
print(f"Field {label}: area = {area:.4f} π")
assert abs(area - 0.5) < 0.05, f"Expected 0.5π, got {area:.4f}π"Field p1 (π/2): area = 0.5000 π
Field p2 (π/2): area = 0.5000 π
Field p3 (π/2): area = 0.5000 π
Summary
- Pulse 1 (π/2) creates a superposition; the macroscopic polarisation free-induction decays on the Doppler timescale \(1/\sigma\).
- Pulse 2 (π/2) converts coherences into a population grating in velocity space; the net coherence vanishes.
- During \(T_w\) the grating persists with amplitude \(e^{-T_w/T_1}\). No coherence is detectable — a key signature of the stimulated echo.
- Pulse 3 (π/2) reads the grating back into coherences.
- Echo at \(t = +\tau\): coherences rephase and the medium emits a coherent burst.
- The \(T_w\)-sweep directly measures \(T_1\); a τ-sweep (as in the two-pulse photon echo example) measures \(T_2\). Together they give the full relaxation picture.
Applications include quantum memory characterisation, optical spectroscopy of inhomogeneously broadened systems (rare-earth-doped crystals, atomic vapours), and the optical analogue of NMR relaxation measurements.
References
- E. L. Hahn, Spin Echoes, Phys. Rev. 80, 580 (1950). Original NMR stimulated echo.
- T. W. Mossberg, Time-domain frequency-selective optical data storage, Opt. Lett. 7, 77 (1982). Optical stimulated echo; \(T_1/T_2\) separation.
- N. A. Kurnit, I. D. Abella, S. R. Hartmann, Observation of a Photon Echo, PRL 13, 567 (1964). First optical photon echo (context).
- L. Allen, J. H. Eberly, Optical Resonance and Two-Level Atoms (Dover, 1987), Ch. 9.