Linear Absorption — Beer–Lambert Law
When an electromagnetic field is weak enough that it never significantly depletes the ground-state population, the medium responds linearly: the field amplitude decays exponentially with propagation distance. This is the Beer–Lambert law,
where the absorption coefficient is \(\alpha(\omega) := k\chi_I(\omega)\) (the wavenumber times the imaginary part of the susceptibility). Since \(I \propto |\Omega|^2\) this is equivalent to
For the resonant two-level system \(\alpha(0) = 2Ng/\Gamma\), giving \(|\Omega(z)| = |\Omega_0|\,e^{-Ng\,z/\Gamma}\) at \(\omega = 0\). We use \(\Omega_0 = 10^{-3}\,\Gamma\) throughout.
This notebook demonstrates:
How increasing optical depth attenuates the pulse.
The role of spontaneous decay in reaching a true Beer–Lambert steady state.
That the exponential attenuation is independent of pulse shape (Gaussian, CW).
Parameters
The interaction_strengths parameter \(Ng\) sets the coupling between field and medium. It is proportional to atomic density \(N\) and the dipole matrix element squared: \(C \propto N |\mu|^2 / \hbar\). In the weak-field limit the resonant absorption coefficient is \(\alpha \propto Ng\), so doubling \(Ng\) doubles the optical depth of the medium.
We will compare three values:
\(Ng\) |
Character |
|---|---|
0.1 |
Optically thin — pulse passes through almost unchanged |
1.0 |
Moderate absorption |
10.0 |
Optically thick — strong attenuation |
[1]:
import numpy as np
import plotly.graph_objects as go
from maxwellbloch import mb_solve, plot
Optically thin medium — \(Ng = 0.1\)
With very few atoms the pulse propagates essentially undistorted. There is a small reduction in amplitude and a slight group delay, but the pulse shape is preserved.
[2]:
mb_solve_json_ng01 = """
{
"atom": {
"fields": [
{
"coupled_levels": [[0, 1]],
"detuning": 0.0,
"rabi_freq": 1.0e-3,
"rabi_freq_t_args": {"ampl": 1.0, "centre": 0.0, "fwhm": 1.0},
"rabi_freq_t_func": "gaussian"
}
],
"num_states": 2,
"decays": [{"channels": [[0, 1]], "rate": 1.0}]
},
"t_min": -2.0,
"t_max": 10.0,
"t_steps": 100,
"z_min": -0.2,
"z_max": 1.2,
"z_steps": 10,
"interaction_strengths": [0.1],
"savefile": "mbs-linear-absorption-ng01-decay"
}
"""
mbs_ng01 = mb_solve.MBSolve().from_json_str(mb_solve_json_ng01)
mbs_ng01.mbsolve(recalc=False);
/home/docs/checkouts/readthedocs.org/user_builds/maxwellbloch/envs/latest/lib/python3.11/site-packages/maxwellbloch/mb_solve.py:344: UserWarning: Savefile was built with maxwellbloch==0.11.0, current version is 0.12.0.
self.load_results()
[3]:
fig = plot.field_spacetime(mbs_ng01)
fig.update_layout(title="|Ω(z, t)| — Gaussian pulse, Ng = 0.1 (optically thin)")
fig.show(renderer='notebook_connected')
Moderate optical depth — \(Ng = 1\)
With \(Ng = 1\) the absorption is noticeable: the output amplitude is appreciably smaller than the input and the pulse acquires a clear group delay as the leading edge drives the atoms before the trailing edge arrives.
[4]:
mb_solve_json_ng1 = """
{
"atom": {
"fields": [
{
"coupled_levels": [[0, 1]],
"detuning": 0.0,
"rabi_freq": 1.0e-3,
"rabi_freq_t_args": {"ampl": 1.0, "centre": 0.0, "fwhm": 1.0},
"rabi_freq_t_func": "gaussian"
}
],
"num_states": 2,
"decays": [{"channels": [[0, 1]], "rate": 1.0}]
},
"t_min": -2.0,
"t_max": 10.0,
"t_steps": 100,
"z_min": -0.2,
"z_max": 1.2,
"z_steps": 20,
"z_steps_inner": 2,
"interaction_strengths": [1.0],
"savefile": "mbs-linear-absorption-ng1-decay"
}
"""
mbs_ng1 = mb_solve.MBSolve().from_json_str(mb_solve_json_ng1)
mbs_ng1.mbsolve(recalc=False);
/home/docs/checkouts/readthedocs.org/user_builds/maxwellbloch/envs/latest/lib/python3.11/site-packages/maxwellbloch/mb_solve.py:344: UserWarning: Savefile was built with maxwellbloch==0.11.0, current version is 0.12.0.
self.load_results()
[5]:
fig = plot.field_spacetime(mbs_ng1)
fig.update_layout(title="|Ω(z, t)| — Gaussian pulse, Ng = 1")
fig.show(renderer='notebook_connected')
Optically thick medium — \(Ng = 10\)
With \(Ng = 10\) the medium is strongly absorbing. The pulse is heavily attenuated and significantly reshaped: the leading spectral components are absorbed first, delaying the effective peak and broadening the transmitted pulse.
[6]:
mb_solve_json_ng10 = """
{
"atom": {
"fields": [
{
"coupled_levels": [[0, 1]],
"detuning": 0.0,
"rabi_freq": 1.0e-3,
"rabi_freq_t_args": {"ampl": 1.0, "centre": 0.0, "fwhm": 1.0},
"rabi_freq_t_func": "gaussian"
}
],
"num_states": 2,
"decays": [{"channels": [[0, 1]], "rate": 1.0}]
},
"t_min": -2.0,
"t_max": 10.0,
"t_steps": 100,
"z_min": -0.2,
"z_max": 1.2,
"z_steps": 100,
"z_steps_inner": 2,
"interaction_strengths": [10.0],
"savefile": "mbs-linear-absorption-ng10-decay"
}
"""
mbs_ng10 = mb_solve.MBSolve().from_json_str(mb_solve_json_ng10)
mbs_ng10.mbsolve(recalc=False);
/home/docs/checkouts/readthedocs.org/user_builds/maxwellbloch/envs/latest/lib/python3.11/site-packages/maxwellbloch/mb_solve.py:344: UserWarning: Savefile was built with maxwellbloch==0.11.0, current version is 0.12.0.
self.load_results()
[7]:
fig = plot.field_spacetime(mbs_ng10)
fig.update_layout(title="|Ω(z, t)| — Gaussian pulse, Ng = 10 (optically thick)")
fig.show(renderer='notebook_connected')
Output envelope comparison
Plotting the output field envelope \(|\Omega(z=1, t)|\) for all three cases alongside the common input shows the progressive attenuation. In the Beer–Lambert limit the peak amplitude scales as \(e^{-\alpha/2}\) with \(\alpha \propto Ng\).
[8]:
fig = go.Figure()
# input (same for all runs — use Ng=0.1 z=0 as reference)
fig.add_trace(go.Scatter(
x=mbs_ng01.tlist,
y=np.abs(mbs_ng01.Omegas_zt[0, 0, :]),
mode="lines",
name="input",
line=dict(dash="dash", color="black"),
))
for mbs, label, color in [
(mbs_ng01, "Ng = 0.1", "#636EFA"),
(mbs_ng1, "Ng = 1", "#EF553B"),
(mbs_ng10, "Ng = 10", "#00CC96"),
]:
fig.add_trace(go.Scatter(
x=mbs.tlist,
y=np.abs(mbs.Omegas_zt[0, -1, :]),
mode="lines",
name=label,
line=dict(color=color),
))
fig.update_layout(
title="Output pulse envelope |Ω(z=1, t)| vs optical depth",
xaxis_title="t (γ⁻¹)",
yaxis_title="|Ω| (γ)",
template="maxwellbloch",
width=700,
height=400,
)
fig.show(renderer='notebook_connected')
Pulse-shape independence — CW field
Beer–Lambert attenuation depends only on frequency and optical depth, not on pulse shape. A continuous-wave (CW) field ramped on at \(t = 0\) reaches a steady-state transmitted amplitude determined solely by \(Ng\) once the transient has decayed. This is the same \(e^{-\alpha/2}\) factor seen above.
With decay present the transient ring-on settles in a time \(\sim 1/\Gamma\).
[9]:
mb_solve_json_cw = """
{
"atom": {
"decays": [{"channels": [[0, 1]], "rate": 1.0}],
"fields": [
{
"coupled_levels": [[0, 1]],
"detuning": 0.0,
"rabi_freq": 1.0e-3,
"rabi_freq_t_args": {"ampl": 1.0, "on": 0.0, "off": 8.0, "fwhm": 1.0},
"rabi_freq_t_func": "ramp_onoff"
}
],
"num_states": 2
},
"t_min": -2.0,
"t_max": 10.0,
"t_steps": 100,
"z_min": -0.2,
"z_max": 1.2,
"z_steps": 20,
"interaction_strengths": [10.0],
"savefile": "mbs-linear-absorption-cw"
}
"""
mbs_cw = mb_solve.MBSolve().from_json_str(mb_solve_json_cw)
mbs_cw.mbsolve(recalc=False);
/home/docs/checkouts/readthedocs.org/user_builds/maxwellbloch/envs/latest/lib/python3.11/site-packages/maxwellbloch/mb_solve.py:344: UserWarning: Savefile was built with maxwellbloch==0.11.0, current version is 0.12.0.
self.load_results()
[10]:
fig = plot.field_spacetime(mbs_cw)
fig.update_layout(title="|Ω(z, t)| — CW ramp-on field, Ng = 10, Γ = 1")
fig.show(renderer='notebook_connected')
Beer–Lambert comparison
With decay and a CW field, the medium reaches a true steady state: the on-resonance (\(\omega = 0\)) field amplitude decays exponentially with penetration depth. Extracting the spatial profile at a time well inside the CW plateau (\(t = 5\,\gamma^{-1}\), long after the ramp-on transient has decayed) and comparing with the Beer–Lambert prediction \(|\Omega(z)| = |\Omega_0|\,e^{-Ng\,z/\Gamma}\) shows excellent agreement.
[11]:
# Time index for plateau (t ≈ 5, well after ramp-on transient)
i_t5 = np.argmin(np.abs(mbs_cw.tlist - 5.0))
# CW amplitude vs z at steady state
Omega_cw_z = np.abs(mbs_cw.Omegas_zt[0, :, i_t5])
# Reference amplitude before the medium (z < 0, vacuum region)
Omega_0 = Omega_cw_z[0] # at z = z_min = -0.2
# Beer-Lambert curve: Ω(z) = Ω₀ exp(−Ng z/Γ) inside medium [0, 1]
# outside [0, 1] the density is zero so amplitude is constant
Ng_val = 10.0
Gamma = 1.0
z_arr = mbs_cw.zlist
beer_lambert = Omega_0 * np.exp(-Ng_val / Gamma * np.clip(z_arr, 0.0, 1.0))
fig = go.Figure()
fig.add_trace(go.Scatter(
x=z_arr, y=Omega_cw_z,
mode='lines', name='simulation (CW steady state)',
))
fig.add_trace(go.Scatter(
x=z_arr, y=beer_lambert,
mode='lines', name='Beer–Lambert: Ω₀ exp(−Ng z/Γ)',
line=dict(dash='dash'),
))
fig.update_layout(
title='CW steady-state spatial profile vs Beer–Lambert (Ng = 10, Γ = 1)',
xaxis_title='z',
yaxis_title='|Ω| (γ)',
yaxis_type='log',
template='maxwellbloch',
width=700,
height=400,
)
fig.show(renderer='notebook_connected')
Summary
In the weak-field limit (\(\Omega_0 \ll \Gamma\)) the Maxwell–Bloch equations reduce to the Beer–Lambert law: amplitude decays exponentially with \(Ng\).
Without spontaneous decay the atoms respond coherently; the medium acts as a resonant absorber but also re-emits energy, producing a free induction decay tail after the pulse.
Adding decay at rate \(\Gamma\) damps the coherence on timescale \(T_2 = 2/\Gamma\). Once \(T_2 \lesssim \tau_\text{pulse}\) the transmission matches the steady-state Beer–Lambert prediction.
The steady-state attenuation is pulse-shape independent: both Gaussian and CW fields with the same \(Ng\) reach the same transmitted amplitude once transients have decayed.
The nonlinear regime — where the field is strong enough to saturate the transition and the area theorem governs propagation — is covered in the Area Theorem and SIT Solitons notebooks.