From Classical to Quantum: When Does It Matter?¶
In the canonical ensemble we derived the partition function for indistinguishable non-interacting particles:
where is the thermal de Broglie wavelength.
This classical (Maxwell-Boltzmann) approximation is valid when the mean occupation of every single-particle state is much less than one:
where is the number density. The dimensionless quantity compares the thermal wavelength to the mean interparticle spacing:
| Regime | Condition | Statistics | Examples |
|---|---|---|---|
| Classical | Maxwell-Boltzmann | Gases at room , solutions, biomolecules | |
| Quantum | Bose-Einstein or Fermi-Dirac | Electrons in metals, He at 2K, neutron stars |
Quantum statistics matters when particles are light (small large ), cold (small large ), or dense (large ).
Source
import numpy as np
import matplotlib.pyplot as plt
from scipy.constants import h, k, N_A, m_e, m_p
# Thermal de Broglie wavelength
def lambda_dB(m, T):
return h / np.sqrt(2 * np.pi * m * k * T)
T = np.linspace(1, 1000, 500)
systems = {
'Electron': (m_e, 8.5e28, '#d62728'), # free electron in copper
r'$^4$He atom': (4*m_p, 2.2e28, '#1f77b4'), # liquid helium density
r'$H_2$ gas (1 atm)': (2*m_p, 2.7e25, '#2ca02c'), # H2 at STP
r'$N_2$ gas (1 atm)': (28*m_p, 2.7e25, '#ff7f0e'), # N2 at STP
}
fig, ax = plt.subplots(figsize=(9, 5))
for label, (m, n, color) in systems.items():
lam = lambda_dB(m, T)
ax.semilogy(T, n * lam**3, lw=2.5, color=color, label=label)
ax.axhline(1, color='gray', ls='--', lw=1.5)
ax.fill_between(T, 1, 1e6, alpha=0.08, color='blue')
ax.fill_between(T, 1e-30, 1, alpha=0.08, color='green')
ax.text(50, 30, 'Quantum regime', fontsize=13, color='blue', fontweight='bold')
ax.text(500, 1e-8, 'Classical regime', fontsize=13, color='green', fontweight='bold')
ax.set_xlabel('Temperature (K)', fontsize=12)
ax.set_ylabel(r'$n\lambda^3$', fontsize=14)
ax.set_title('Classical vs Quantum Regime', fontsize=14, fontweight='bold')
ax.set_ylim(1e-15, 1e5)
ax.set_xlim(1, 1000)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3, which='both')
plt.tight_layout()
plt.show()
Occupation Number Representation¶
For non-interacting particles, a microstate is specified by the occupation numbers of single-particle energy levels :
The key question is: what values can take?
| Particle type | Spin | Allowed | Example |
|---|---|---|---|
| Bosons | Integer (0, 1, 2, ...) | Photons, phonons, He | |
| Fermions | Half-integer (1/2, 3/2, ...) | 0 or 1 only | Electrons, protons, He |
This single constraint — whether particles can share quantum states — leads to profoundly different statistical behavior.
The Three Distribution Laws¶
Working in the grand canonical ensemble (where the constraint is relaxed), the partition function factorizes over single-particle levels:
Why the grand canonical ensemble?
In the canonical ensemble, the constraint couples all occupation numbers and prevents the partition function from factorizing. In the grand canonical ensemble, is allowed to fluctuate, and the sum over all becomes unrestricted:
Each factor is a geometric series:
Bosons ():
Fermions ():
The mean occupation follows from , and fluctuations from .
The resulting mean occupation number takes a universal form:
Bose-Einstein: Occupation is unbounded. Bosons tend to bunch into the same state. At , macroscopic occupation of the ground state leads to Bose-Einstein condensation. Note: for the ground state (), requires to keep occupation non-negative. The chemical potential of a boson gas is always less than or equal to the ground state energy.
Fermi-Dirac: Occupation is 0 or 1 (Pauli exclusion). Fermions avoid each other. At , all states below the Fermi energy are filled, forming a sharp step function.
Maxwell-Boltzmann: The classical limit recovered when (i.e. , low density), where both quantum distributions converge to .
Source
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-2, 8, 500) # x = beta*(epsilon - mu)
# Avoid division by zero
be = 1.0 / (np.exp(x) - 1) # Bose-Einstein
fd = 1.0 / (np.exp(x) + 1) # Fermi-Dirac
mb = np.exp(-x) # Maxwell-Boltzmann
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Left panel: all three distributions
ax = axes[0]
ax.plot(x, be, lw=2.5, color='#1f77b4', label='Bose-Einstein ($c=-1$)')
ax.plot(x, fd, lw=2.5, color='#d62728', label='Fermi-Dirac ($c=+1$)')
ax.plot(x, mb, lw=2.5, color='#2ca02c', ls='--', label='Maxwell-Boltzmann ($c=0$)')
ax.axvline(0, color='gray', ls=':', lw=1)
ax.set_xlabel(r'$\beta(\epsilon_i - \mu)$', fontsize=13)
ax.set_ylabel(r'$\langle n_i \rangle$', fontsize=13)
ax.set_title('Quantum vs Classical Distributions', fontsize=13, fontweight='bold')
ax.set_xlim(-2, 6)
ax.set_ylim(0, 4)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Right panel: Fermi-Dirac at different temperatures
ax = axes[1]
eps = np.linspace(0, 15, 500) # energy in units of some scale
mu_val = 7.0 # Fermi energy
for kT, color in zip([0.2, 0.5, 1.0, 2.0, 5.0],
['#1a1a2e', '#16213e', '#0f3460', '#e94560', '#ff9a00']):
n_fd = 1.0 / (np.exp((eps - mu_val) / kT) + 1)
ax.plot(eps, n_fd, lw=2, color=color, label=rf'$k_BT = {kT}$')
ax.axvline(mu_val, color='gray', ls='--', lw=1.5)
ax.text(mu_val + 0.2, 0.85, r'$\epsilon_F = \mu$', fontsize=11, color='gray')
ax.set_xlabel(r'$\epsilon$', fontsize=13)
ax.set_ylabel(r'$\langle n \rangle$', fontsize=13)
ax.set_title('Fermi-Dirac: Sharpness at Low $T$', fontsize=13, fontweight='bold')
ax.legend(fontsize=10)
ax.set_ylim(-0.05, 1.1)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Density of States¶
To compute thermodynamic quantities, we convert sums over discrete levels to integrals using the density of states :
For free particles in 3D with :
Derivation of from k-space counting
The total energy and particle number become:
Application 1: Blackbody Radiation (Photon Gas)¶
Photons are massless bosons with (photon number is not conserved). Applying Bose-Einstein statistics with the photon density of states gives:
Key results:
Wien’s displacement law: Peak frequency shifts
Stefan-Boltzmann law: Total energy density
Classical limit (): Rayleigh-Jeans formula , which diverges at high (ultraviolet catastrophe)
Source
import numpy as np
import matplotlib.pyplot as plt
from scipy.constants import c, h, k
def planck(wav, T):
"""Planck spectral radiance as a function of wavelength."""
return 2.0 * h * c**2 / (wav**5 * (np.exp(h * c / (wav * k * T)) - 1.0))
wavs = np.arange(100e-9, 3000e-9, 5e-9)
fig, ax = plt.subplots(figsize=(9, 5))
colors = ['#1a1a2e', '#0f3460', '#e94560', '#ff9a00', '#ffcc00']
for T_val, color in zip([3000, 4000, 5000, 6000, 7000], colors):
ax.plot(wavs * 1e9, planck(wavs, T_val) * 1e-12, lw=2.5, color=color,
label=f'{T_val} K')
ax.set_xlabel(r'Wavelength (nm)', fontsize=12)
ax.set_ylabel(r'Spectral radiance (TW/m$^3$/sr)', fontsize=12)
ax.set_title('Planck Blackbody Spectrum', fontsize=14, fontweight='bold')
ax.set_xlim(0, 2500)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
# Shade visible range
ax.axvspan(380, 750, alpha=0.1, color='orange', label='Visible')
ax.text(550, ax.get_ylim()[1]*0.9, 'Visible', ha='center', fontsize=10,
color='orange', fontweight='bold')
plt.tight_layout()
plt.show()
Application 2: Heat Capacity of Solids (Phonon Gas)¶
Atoms in a crystal vibrate as coupled harmonic oscillators. In normal-mode coordinates, the solid becomes a gas of phonons — quantized lattice vibrations that obey Bose-Einstein statistics with .
Why for phonons (and photons)?
The partition function for a quantum harmonic oscillator with frequency is:
The second factor is exactly the bosonic grand canonical partition function with . This is because the number of phonons (or photons) is not conserved — they are created and destroyed freely as the crystal vibrates. Since there is no constraint on total phonon number, no chemical potential is needed to enforce it.
The mean occupation of mode is:
where the is the quantum zero-point contribution.
The Debye model approximates the phonon density of states as up to a cutoff (set by requiring total modes). The resulting heat capacity is:
where is the Debye temperature.
| Limit | Physics | |
|---|---|---|
| (Dulong-Petit) | All modes classically excited | |
| Only low-frequency acoustic modes are excited |
Source
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import quad
from scipy.constants import k, N_A
def debye_cv(T_arr, T_D):
"""Debye heat capacity per mole."""
def integrand(x):
return x**4 * np.exp(x) / (np.exp(x) - 1)**2
cv = np.zeros_like(T_arr)
for i, T in enumerate(T_arr):
if T < 0.01:
cv[i] = 0
else:
cv[i] = 9 * N_A * k * (T / T_D)**3 * quad(integrand, 0, T_D / T)[0]
return cv
T = np.linspace(1, 600, 500)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
# Left: C_v vs T for different Debye temperatures
materials = {'Diamond (2230 K)': 2230, 'Silicon (645 K)': 645,
'Copper (343 K)': 343, 'Lead (105 K)': 105}
colors = ['#1f77b4', '#2ca02c', '#ff7f0e', '#d62728']
for (name, T_D), color in zip(materials.items(), colors):
cv = debye_cv(T, T_D)
ax1.plot(T, cv, lw=2.5, color=color, label=name)
ax1.axhline(3 * N_A * k, color='gray', ls='--', lw=1.5)
ax1.text(450, 3 * N_A * k + 0.5, 'Dulong-Petit: $3Nk_B$', fontsize=10, color='gray')
ax1.set_xlabel('Temperature (K)', fontsize=12)
ax1.set_ylabel(r'$C_V$ (J/mol/K)', fontsize=12)
ax1.set_title('Debye Heat Capacity', fontsize=13, fontweight='bold')
ax1.legend(fontsize=10)
ax1.grid(True, alpha=0.3)
# Right: Universal curve C_v vs T/T_D
T_reduced = np.linspace(0.01, 3, 500)
cv_universal = debye_cv(T_reduced * 343, 343) / (3 * N_A * k) # normalized
ax2.plot(T_reduced, cv_universal, lw=3, color='steelblue')
ax2.set_xlabel(r'$T / \Theta_D$', fontsize=13)
ax2.set_ylabel(r'$C_V / 3Nk_B$', fontsize=13)
ax2.set_title('Universal Debye Curve', fontsize=13, fontweight='bold')
ax2.axhline(1, color='gray', ls='--', lw=1.5)
# Show T^3 behavior
T_low = np.linspace(0.01, 0.3, 100)
cv_low = debye_cv(T_low * 343, 343) / (3 * N_A * k)
ax2.plot(T_low, cv_low, lw=3, color='steelblue')
ax2.annotate(r'$\propto T^3$', xy=(0.15, 0.02), fontsize=14,
xytext=(0.5, 0.15), color='red', fontweight='bold',
arrowprops=dict(arrowstyle='->', color='red', lw=1.5))
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()/tmp/ipykernel_3048/2195249607.py:9: RuntimeWarning: overflow encountered in exp
return x**4 * np.exp(x) / (np.exp(x) - 1)**2
/tmp/ipykernel_3048/2195249607.py:9: RuntimeWarning: invalid value encountered in scalar divide
return x**4 * np.exp(x) / (np.exp(x) - 1)**2
/tmp/ipykernel_3048/2195249607.py:9: RuntimeWarning: overflow encountered in scalar power
return x**4 * np.exp(x) / (np.exp(x) - 1)**2
/tmp/ipykernel_3048/2195249607.py:9: RuntimeWarning: overflow encountered in scalar multiply
return x**4 * np.exp(x) / (np.exp(x) - 1)**2
/tmp/ipykernel_3048/2195249607.py:16: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
the requested tolerance from being achieved. The error may be
underestimated.
cv[i] = 9 * N_A * k * (T / T_D)**3 * quad(integrand, 0, T_D / T)[0]

Application 3: Free Electron Gas (Fermi Gas)¶
Electrons in a metal are fermions. At , they fill all available states up to the Fermi energy:
For typical metals, eV, corresponding to a Fermi temperature K. Since room temperature , the electron gas is deeply quantum even at 300 K.
Key consequences:
Only electrons within of contribute to heat capacity: (linear, not the classical )
Even at , electrons exert degeneracy pressure (Pauli exclusion prevents collapse)
This explains why metals are good conductors but have surprisingly small electronic heat capacity
Source
import numpy as np
import matplotlib.pyplot as plt
# Fermi gas: occupation * density of states at different T
eps = np.linspace(0, 12, 500)
mu = 7.0 # eV, typical Fermi energy
fig, ax = plt.subplots(figsize=(9, 5))
# Density of states g(e) ~ sqrt(e)
dos = np.sqrt(eps)
for kT, alpha, color in [(0.05, 0.9, '#1a1a2e'), (0.2, 0.7, '#0f3460'),
(0.5, 0.5, '#e94560'), (1.5, 0.4, '#ff9a00')]:
fd = 1.0 / (np.exp((eps - mu) / kT) + 1)
occupied_dos = dos * fd
ax.fill_between(eps, 0, occupied_dos, alpha=alpha * 0.4, color=color)
ax.plot(eps, occupied_dos, lw=2, color=color, label=rf'$k_BT = {kT}$ eV')
# Show full DOS for reference
ax.plot(eps, dos, lw=1.5, color='gray', ls='--', label=r'$g(\epsilon) \propto \sqrt{\epsilon}$')
ax.axvline(mu, color='black', ls=':', lw=1.5)
ax.text(mu + 0.15, ax.get_ylim()[1]*0.55, r'$\epsilon_F$', fontsize=14, fontweight='bold')
ax.set_xlabel(r'Energy $\epsilon$ (eV)', fontsize=12)
ax.set_ylabel(r'$g(\epsilon) \langle n(\epsilon) \rangle$', fontsize=12)
ax.set_title('Occupied States in a Free Electron Gas', fontsize=14, fontweight='bold')
ax.legend(fontsize=10)
ax.set_xlim(0, 12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Summary: When to Use What¶
| System | Statistics | Why |
|---|---|---|
| Gas molecules at room | Maxwell-Boltzmann | |
| Molecules in solution | Maxwell-Boltzmann | Dilute, heavy, warm |
| Photons (radiation) | Bose-Einstein () | Massless bosons |
| Phonons (lattice vibrations) | Bose-Einstein () | Quantized collective modes |
| Electrons in metals | Fermi-Dirac | K |
| Liquid He below 2.17 K | Bose-Einstein | Superfluid transition |
| Liquid He | Fermi-Dirac | Fermionic helium isotope |
Problems¶
Show that both Bose-Einstein and Fermi-Dirac distributions reduce to the Maxwell-Boltzmann distribution in the limit . What physical condition does this correspond to?
For a 2D free electron gas (relevant to graphene), the density of states is constant: where is the area. Derive the Fermi energy and show that at low temperatures.
Use the Planck distribution to show that the total energy density of a photon gas scales as (Stefan-Boltzmann law). Hint: substitute and use .
Compute the occupation number fluctuations for bosons and fermions. Show that:
Bosons: — super-Poissonian (bunching)
Fermions: — sub-Poissonian (antibunching)
Classical: — Poissonian
Estimate for: (a) nitrogen gas at STP, (b) electrons in copper ( m), (c) water molecules in liquid water. Which systems require quantum statistics?