Note
Go to the end to download the full example code.
The Hanbury Brown-Twiss effect: bunching, coherent, and antibunched light#
Hanbury Brown and Twiss measured intensity correlations between two separated detectors and found bunching in thermal starlight – the second-order correlation function
came out at 2 for their chaotic (thermal) source, rather than the value
1 a classical coherent wave gives. This example computes
\(g^{(2)}(0)\) directly from photon-number distributions for three
cases: a thermal (chaotic) source, built directly from its geometric
number distribution; a coherent state, from
coherent_state(); and a Fock
state, from fock_state() –
recovering the historical bunched (2), classical boundary (1), and
antibunched (<1) values in one place, and showing where Kimble,
Dagenais, and Mandel’s 1977 sub-Poissonian measurement sits relative to
Hanbury Brown and Twiss’s own bunched result.
import matplotlib.pyplot as plt
import numpy as np
from physicskit.optics.quantum_optics import coherent_state, fock_state
g^(2)(0) from a photon-number distribution#
For any (mixed or pure) photon-number distribution P(n), the zero-delay second-order correlation is exactly the ratio of the second factorial moment to the mean squared – no field amplitudes or detector geometry are needed, only the number statistics.
Thermal (chaotic) light: Hanbury Brown and Twiss’s actual source#
Thermal light has no single quantum state – it is a statistical mixture with the geometric (Bose-Einstein) number distribution P(n) = nbar^n / (1+nbar)^(n+1). Built directly here, exactly the classical-statistics picture that explained the bunching HBT measured.
Coherent light: the classical Poissonian boundary#
alpha = np.sqrt(n_bar)
psi_coherent = coherent_state(alpha, n_max)
P_coherent = np.abs(psi_coherent) ** 2
g2_coherent = g2_from_distribution(P_coherent)
A Fock state: fully antibunched (Kimble, Dagenais, and Mandel’s regime)#
k = 3
psi_fock = fock_state(k, n_max)
P_fock = np.abs(psi_fock) ** 2
g2_fock = g2_from_distribution(P_fock)
print(f"thermal (n_bar={n_bar}): g2(0) = {g2_thermal:.4f} (Hanbury Brown-Twiss found 2 -- bunched)")
print(f"coherent (<n>={n_bar}): g2(0) = {g2_coherent:.4f} (the classical boundary -- neither bunched nor antibunched)")
print(f"Fock |{k}>: g2(0) = {g2_fock:.4f} (Kimble-Dagenais-Mandel's regime -- antibunched, no classical field can do this)")
thermal (n_bar=3.0): g2(0) = 2.0000 (Hanbury Brown-Twiss found 2 -- bunched)
coherent (<n>=3.0): g2(0) = 1.0000 (the classical boundary -- neither bunched nor antibunched)
Fock |3>: g2(0) = 0.6667 (Kimble-Dagenais-Mandel's regime -- antibunched, no classical field can do this)
All three, side by side#
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))
width = 0.8
ax1.bar(n[:15], P_thermal[:15], width, alpha=0.7, label=f"thermal, g2={g2_thermal:.2f}", color="firebrick")
ax1.bar(n[:15], P_coherent[:15], width, alpha=0.5, label=f"coherent, g2={g2_coherent:.2f}", color="steelblue")
ax1.set_xlabel("photon number n")
ax1.set_ylabel("P(n)")
ax1.set_title("Thermal vs. coherent photon-number distributions")
ax1.legend(fontsize=8)
labels = ["thermal\n(bunched)", "coherent\n(classical boundary)", f"Fock |{k}>\n(antibunched)"]
values = [g2_thermal, g2_coherent, g2_fock]
colors = ["firebrick", "steelblue", "seagreen"]
ax2.bar(labels, values, color=colors)
ax2.axhline(1.0, color="0.4", ls="--", lw=1, label="classical coherent boundary")
ax2.set_ylabel(r"$g^{(2)}(0)$")
ax2.set_title("The full range Hanbury Brown-Twiss's technique measures")
ax2.legend(fontsize=8)
fig.tight_layout()
plt.show()

Total running time of the script: (0 minutes 0.078 seconds)