codexplanck-frequency-and-wavelength-peaks.py
planck-frequency-and-wavelength-peaks.py
"""One 6000 K Planck spectrum in frequency and wavelength coordinates.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Writes planck-frequency-and-wavelength-peaks.png into the working directory.
Peak equations use standard-library bisection, with no SciPy dependency.
"""
import math
import matplotlib.pyplot as plt
import numpy as np
H = 6.62607015e-34
K_B = 1.380649e-23
C = 299792458.0
TEMPERATURE = 6000.0
def positive_peak_root(exponent):
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_nu(frequency_hz):
return (2 * H * frequency_hz**3 / C**2
/ np.expm1(H * frequency_hz / (K_B * TEMPERATURE)))
def b_lambda(wavelength_m):
return (2 * H * C**2 / wavelength_m**5
/ np.expm1(H * C / (wavelength_m * K_B * TEMPERATURE)))
def main():
x_nu, x_lambda = (positive_peak_root(m) for m in (3, 5))
nu_peak = x_nu * K_B * TEMPERATURE / H
lambda_peak = H * C / (K_B * TEMPERATURE * x_lambda)
converted_lambda = C / nu_peak
converted_nu = C / lambda_peak
assert math.isclose(converted_lambda * 1e6, 0.84990713825, rel_tol=1e-10)
assert math.isclose(lambda_peak * 1e6, 0.482961992531, rel_tol=1e-10)
# Check both density transformation and equal power in matching finite bands.
band_lambda = np.linspace(0.4e-6, 0.8e-6, 20001)
assert np.allclose(b_lambda(band_lambda),
b_nu(C / band_lambda) * C / band_lambda**2,
rtol=5e-14, atol=0)
band_nu = np.linspace(C / band_lambda[-1], C / band_lambda[0], 20001)
wavelength_integral = np.trapezoid(b_lambda(band_lambda), band_lambda)
frequency_integral = np.trapezoid(b_nu(band_nu), band_nu)
assert math.isclose(wavelength_integral, frequency_integral, rel_tol=1e-8)
for peak, density in ((nu_peak, b_nu), (lambda_peak, b_lambda)):
assert density(peak) > density(peak * 0.99)
assert density(peak) > density(peak * 1.01)
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white",
"axes.spines.top": False, "axes.spines.right": False})
fig, (freq_ax, wave_ax) = plt.subplots(1, 2, figsize=(11.2, 5.7), dpi=120)
fig.subplots_adjust(left=0.08, right=0.97, bottom=0.26, top=0.79, wspace=0.34)
fig.suptitle("Same 6000 K radiation: the peak depends on spectral coordinates",
fontsize=15)
blue, orange = "#245caa", "#a14b15"
nu_thz = np.linspace(1, 1500, 2400)
lambda_um = np.linspace(0.1, 3, 2400)
# Per Hz -> per THz multiplies by 1e12; per m -> per um by 1e-6.
freq_ax.plot(nu_thz, b_nu(nu_thz * 1e12) * 1e12 / 1e4, color=blue, lw=2.6)
wave_ax.plot(lambda_um, b_lambda(lambda_um * 1e-6) * 1e-6 / 1e6,
color=orange, lw=2.6)
freq_ax.set(title="Equal-width frequency bins", xlim=(0, 1500), ylim=(0, 11),
xlabel="Frequency ν (THz)", ylabel="Bν (10⁴ W m⁻² sr⁻¹ THz⁻¹)")
wave_ax.set(title="Equal-width wavelength bins", xlim=(0.1, 3), ylim=(0, 36),
xlabel="Wavelength λ (μm)", ylabel="Bλ (10⁶ W m⁻² sr⁻¹ μm⁻¹)")
for ax, own, other, own_y, other_y, color in (
(freq_ax, nu_peak / 1e12, converted_nu / 1e12,
b_nu(nu_peak) * 1e12 / 1e4, b_nu(converted_nu) * 1e12 / 1e4, blue),
(wave_ax, lambda_peak * 1e6, converted_lambda * 1e6,
b_lambda(lambda_peak) * 1e-6 / 1e6,
b_lambda(converted_lambda) * 1e-6 / 1e6, orange),
):
ax.vlines(own, 0, own_y, color=color, linestyle=":", lw=1.3)
ax.plot(own, own_y, "o", color=color, ms=7)
ax.vlines(other, 0, other_y, color="#555b62", linestyle="--", lw=1.4)
ax.plot(other, other_y, "o", mfc="white", mec="#555b62", ms=7)
ax.grid(alpha=0.18)
freq_ax.annotate(f"Bν peak\n{nu_peak / 1e12:.3f} THz",
(nu_peak / 1e12, b_nu(nu_peak) * 1e12 / 1e4),
xytext=(17, 6), textcoords="offset points", color=blue)
wave_ax.annotate(f"Bλ peak\n{lambda_peak * 1e6:.6f} μm",
(lambda_peak * 1e6, b_lambda(lambda_peak) * 1e-6 / 1e6),
xytext=(20, 4), textcoords="offset points", color=orange)
freq_ax.annotate("c / λpeak\n620.737 THz", (converted_nu / 1e12,
b_nu(converted_nu) * 1e12 / 1e4), xytext=(35, -10),
textcoords="offset points", color="#555b62",
arrowprops={"arrowstyle": "-", "color": "#555b62"})
wave_ax.annotate(f"c / νpeak\n{converted_lambda * 1e6:.6f} μm",
(converted_lambda * 1e6, b_lambda(converted_lambda) * 1e-6 / 1e6),
xytext=(65, 8), textcoords="offset points", color="#555b62",
arrowprops={"arrowstyle": "-", "color": "#555b62"})
fig.text(0.5, 0.115, r"$B_\lambda = B_\nu(c/\lambda)\,c/\lambda^2$", ha="center",
fontsize=16)
fig.text(0.5, 0.045,
"Filled dots: each panel's maximum · open dots: the other peak, converted",
ha="center", fontsize=11)
fig.savefig("planck-frequency-and-wavelength-peaks.png", facecolor="white",
transparent=False)
plt.close(fig)
print(f"x_nu={x_nu:.12f}; x_lambda={x_lambda:.12f}")
print(f"nu_peak={nu_peak / 1e12:.9f} THz; lambda_peak={lambda_peak * 1e6:.9f} um")
print(f"c/nu_peak={converted_lambda * 1e6:.9f} um; "
f"c/lambda_peak={converted_nu / 1e12:.9f} THz")
print(f"0.4–0.8 um band: {wavelength_integral:.9g} W m^-2 sr^-1; "
f"relative coordinate-integral difference "
f"{abs(wavelength_integral / frequency_integral - 1):.3g}")
if __name__ == "__main__":
main()