codexplanck-spectral-radiance.py
planck-spectral-radiance.py
"""Ideal wavelength spectra at three stipulated temperatures.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Writes planck-spectral-radiance.png into the current working directory.
Constants are exact SI values; this is a model, not measured data.
"""
import math
import matplotlib.pyplot as plt
import numpy as np
H = 6.62607015e-34 # J s
K_B = 1.380649e-23 # J/K
C = 299792458.0 # m/s
def positive_peak_root(exponent):
"""Bisect m(1-exp(-x))-x on [1,m], excluding its endpoint root at 0."""
lo, hi = 1.0, float(exponent)
for _ in range(100):
mid = (lo + hi) / 2
if exponent * (-math.expm1(-mid)) - mid > 0:
lo = mid
else:
hi = mid
return (lo + hi) / 2
def b_lambda(wavelength_m, temperature):
return (2 * H * C**2 / wavelength_m**5
/ np.expm1(H * C / (wavelength_m * K_B * temperature)))
def main():
x_peak = positive_peak_root(5)
displacement = H * C / (K_B * x_peak)
temperatures = (3000, 4500, 6000)
peaks_um = np.array([displacement / t * 1e6 for t in temperatures])
assert np.allclose(peaks_um, [0.9659239851, 0.6439493234, 0.4829619925])
for t, peak_um in zip(temperatures, peaks_um):
peak = peak_um * 1e-6
assert b_lambda(peak, t) > b_lambda(peak * 0.99, t)
assert b_lambda(peak, t) > b_lambda(peak * 1.01, t)
wavelength_um = np.linspace(0.1, 3.0, 2200)
wavelength_m = wavelength_um * 1e-6
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white",
"axes.spines.top": False, "axes.spines.right": False})
fig, ax = plt.subplots(figsize=(9.4, 5.3), dpi=130)
fig.subplots_adjust(left=0.115, right=0.965, bottom=0.19, top=0.84)
fig.suptitle("Planck's law: temperature sets the whole spectrum", fontsize=16)
for t, peak_um, color in zip(temperatures, peaks_um,
("#a14b15", "#237850", "#245caa")):
# B_lambda per metre -> per micrometre: multiply by 1e-6.
radiance_per_um = b_lambda(wavelength_m, t) * 1e-6
ax.plot(wavelength_um, radiance_per_um / 1e6, color=color, lw=2.6,
label=f"{t} K · peak {peak_um * 1000:.0f} nm")
peak_y = b_lambda(peak_um * 1e-6, t) * 1e-6 / 1e6
ax.plot(peak_um, peak_y, "o", ms=6, color=color)
ax.vlines(peak_um, 0, peak_y, color=color, linestyle=":", lw=1.2)
ax.set(xlim=(0.1, 3.0), ylim=(0, 35),
xlabel="Wavelength λ (μm)",
ylabel="Bλ (10⁶ W m⁻² sr⁻¹ μm⁻¹)")
ax.set_xticks([0.1, 0.5, 1, 1.5, 2, 2.5, 3])
ax.grid(alpha=0.18)
ax.legend(frameon=False, loc="upper right", fontsize=12)
fig.text(0.5, 0.035,
"Ideal equilibrium spectra · markers maximize radiance per unit wavelength",
ha="center", fontsize=11)
fig.savefig("planck-spectral-radiance.png", facecolor="white", transparent=False)
plt.close(fig)
print(f"x_lambda={x_peak:.12f}; b={displacement:.12g} m K")
for t, peak_um in zip(temperatures, peaks_um):
print(f"T={t} K: lambda_peak={peak_um * 1000:.6f} nm; "
f"B_lambda(0.5 um)={b_lambda(0.5e-6, t) * 1e-6:.6g} per um")
if __name__ == "__main__":
main()