做传热学仿真,绕不开黑体辐射这个话题。主题062就是这样一个练手题目:给你一组温度条件,让你从普朗克定律出发,算出黑体的光谱辐射力分布,再顺手验证维恩位移定律和斯特藩-玻尔兹曼定律。听起来像是课本知识的简单复刻,但真正用程序把它跑出来、把曲线画出来、把数值误差分析清楚,你对辐射传热的理解绝对会上一个台阶。我这次就用Python把整个仿真流程走了一遍,包括建模思路、关键代码、参数选择、结果分析以及踩过的坑,整理成这篇分享,给正在做传热学课程设计、热辐射仿真,或者刚接触数值计算的同学参考。
1. 主题拆解与仿真目标
1.1 黑体辐射到底在算什么问题
黑体是一个理想化模型,它吸收所有入射辐射,不反射也不透射,同时它的辐射能力只取决于自身温度,与材料属性无关。这个模型的价值在于,它把“热辐射”从一堆经验系数里解放出来,给了全世界一个统一的标尺。实际物体的辐射能力,最终都要拿黑体去比较,用发射率ε来修正。
普朗克定律给出的是黑体的光谱辐射力,也就是单位面积、单位波长间隔内黑体向半球空间发射的辐射功率。公式形式不算复杂:
E_bλ(λ, T) = C1 / (λ⁵ × (e^(C2/(λT)) - 1))
其中 C1 = 2πhc² ≈ 3.7418×10⁻16 W·m²,C2 = hc/k ≈ 1.4388×10⁻² m·K。
这里面的C2非常关键,后面所有数值稳定性问题几乎都出在它的指数项上。λ要用米做单位,T用开尔文,指数项 C2/(λT) 必须保持无量纲,一旦单位换算搞错,计算出来的数就会差出好几个数量级。
1.2 为什么需要仿真而不只是看公式
很多同学会觉得,公式都给你了,手算都能出结果,为什么还要写程序仿真?这里有个核心原因:普朗克公式虽然简洁,但它的完整信息量远超课本上那几条定律。
第一,黑体辐射曲线是非线性的,温度一变,峰值位置和曲线整体高度同时变化,单靠笔算只能算几个孤立点,画不出全貌。第二,维恩位移定律和斯特藩-玻尔兹曼定律其实都“藏”在普朗克公式里,但你需要通过数值求导、数值积分才能把它们“还原”出来,这个过程本身就是对物理量的再理解。第三,工程上有大量场景——高温炉膛内的辐射换热、太阳能集热器表面温度估算、红外热成像测温——都需要快速计算黑体辐射在不同温度下的分布,而用程序封装这个计算,一秒钟可以跑成百上千个温度点。
所以,这个主题的仿真目标可以拆成四条:
- 计算并绘制多个温度下的黑体光谱辐射力曲线;
- 通过峰值定位验证维恩位移定律;
- 通过数值积分验证斯特藩-玻尔兹曼定律;
- 结合实际场景,理解温度对辐射能量分布的影响。
目标明确之后,你的仿真就有一个清晰的验收标准:数值结果和理论值偏差在可接受范围内,几条定律能定量复现。
2. 模型构建与关键参数
2.1 光谱辐射力、全波长辐射力与半球空间
先厘清一个容易混淆的概念。普朗克公式给出的是E_bλ,这是“光谱辐射力”,它依赖波长。把E_bλ对所有波长积分,得到的就是全波长辐射力E_b,它的值等于σT⁴,也就是斯特藩-玻尔兹曼定律。这个量描述的是黑体单位面积向半球空间辐射的总功率。
注意“半球空间”这个词。辐射力不是只往某一个方向辐射,而是整个半球空间内所有方向的辐射能量之和。所以E_bλ的单位是W/(m²·μm),这里的分母带了面积和波长,别漏掉任何一个单位。
在仿真建模时,你需要决定算几维问题。如果只关心光谱分布和总辐射力,模型是零维的,本质上就是对一个函数采样、积分,不涉及空间网格。很多第一次做这个题目的同学会莫名其妙地开始搭三维网格、算立体角,其实没必要。先从最基本的“点模型”做起,把普朗克函数算对,后面再谈方向辐射特性和空间离散。
2.2 波长范围、采样点数、归一化处理怎么选
波长范围是第一个要拍板的参数。我试过的经验是取 0.1~50 μm 比较稳妥。0.1μm以下那部分辐射能量占比极小,即使温度到6000K,0.1μm以下的能量份额也基本可以忽略;50μm以上则主要影响低温情况,300K的黑体峰值在9.66μm附近,长波拖尾一直延伸到很远,但超过50μm后的积分贡献比例很小,对验证斯特藩-玻尔兹曼定律影响不超过0.5%。
当然,如果你要更严谨,可以按温度动态调整范围,比如 T=300K 时把上限放到100μm,T=6000K 时把下限放到0.05μm。但作为主题仿真,固定范围已经足够说明物理规律。
采样点数方面,我建议至少2000个点,最优选10000个点。原因在于峰值定位依赖离散数组的索引,如果λ间隔太大,找到的峰值波长会偏移,维恩位移定律的验证误差就会变大。以6000K为例,理论峰值波长约483nm,如果整个0.1~50μm范围只取500个点,每个点间隔约100nm,峰值定位可能偏差几十纳米,乘积算出来就会偏离2898μm·K。点数取到10000后,间隔只有5nm,峰值误差基本可以控制在5nm以内。
还有一个细节:画多个温度曲线时,不同温度下E_bλ的量级差距巨大,6000K的峰值比300K高出十几个数量级。如果直接在同一张图里画线性坐标,低温曲线会变成一条紧贴横轴的水平线,什么都看不清。我一般会画两张图——线性坐标图看趋势,对数坐标图看宽动态范围,并且把每条曲线除以自己的最大值做归一化,这样峰值位置和曲线形状一目了然。
2.3 单位与常数的细节陷阱
单位问题是这个主题里最容易翻车的环节,我排查过很多次,也看到不少同学在这上面卡住。
普朗克公式的指数项是 C2/(λT)。C2的单位是m·K,所以λ必须以米为单位。如果你在程序里用了微米做变量,比如 lam_um = 2.0 代表 2μm,那计算时必须写成 lam = lam_um * 1e-6,这一点漏掉,指数项就会差10⁶倍,结果出来要么是0,要么是inf。
另一个陷阱是C1的数值。C1 = 2πhc² ≈ 3.7418×10⁻16 W·m²。在计算中经常有人写成 3.74×10⁻16,然后忘记乘π或者忘记平方光速,导致光谱辐射力整体偏小或偏大。保险做法是直接用 Python 里 scipy.constants 提供的 h、c、k,让程序自己算C1、C2,最大限度减少手输常数的风险。
还有一个小细节,很多教材会把C1写成 3.742×10⁻16 W·m²,用C1除以λ⁵后单位刚好是W/(m²·μm)还是W/(m²·m)?这里有个单位换算的坑。如果λ用米,C1用W·m²,那 E = C1 / λ⁵ 的单位是 W/m³,要把它转成 W/(m²·μm) 还得除以1e-6的换算系数。这个我在第五部分会重点讲,是很多人结果对不上理论的常见原因。
3. 仿真实现与核心代码
3.1 普朗克函数与常量定义
整个仿真最核心的就一个函数:给定波长和温度,返回光谱辐射力。我用的实现方式如下。
import numpy as np import matplotlib.pyplot as plt from scipy.integrate import simpson from scipy.constants import h, c, k def planck_spectral(lam_m, T): """ 计算黑体光谱辐射力 lam_m: 波长,单位米 T: 温度,单位开尔文 返回 E_b_lambda,单位 W/(m^2*m) """ C1 = 2.0 * np.pi * h * c**2 C2 = h * c / k exponent = C2 / (lam_m * T) with np.errstate(over='ignore'): term = np.exp(exponent) - 1.0 return C1 / (lam_m**5 * term)解释一下这段代码里的两个关键点。第一,C1和C2不是手打的常数,而是从scipy.constants里现取,这样能保证精度。第二,np.errstate(over='ignore') 是为了防止指数项溢出时报Warning干扰阅读,当波长很小或温度很低时,exponent可能超过700,exp函数会返回inf,与其让程序刷屏报错,不如静默处理,后面再统一排查非法值。
返回单位是W/(m²·m),这和你最终想要的W/(m²·μm)还差一个单位换算。我习惯在调用处统一转换:把波长传入时用米,绘图时转换成微米,把辐射力换算成W/(m²·μm),这样物理量清晰,不容易混。
3.2 批量计算温度曲线与归一化展示
有了核心函数,后面就简单了。我定义一个温度列表,覆盖从室温到太阳表面温度的跨度,然后循环计算。
T_list = [300, 600, 1000, 2000, 6000] lam = np.linspace(0.1e-6, 50e-6, 10000) fig, axes = plt.subplots(1, 2, figsize=(12, 4.5)) for T in T_list: E = planck_spectral(lam, T) E_per_um = E * 1e-6 # 转换为 W/(m^2*um) lam_um = lam * 1e6 axes[0].plot(lam_um, E_per_um, label=f'T={T}K') axes[1].plot(lam_um, E_per_um / E_per_um.max(), label=f'T={T}K') axes[0].set_xlabel('波长 (μm)') axes[0].set_ylabel('光谱辐射力 E_bλ (W/(m²·μm))') axes[0].set_title('线性坐标') axes[0].legend() axes[1].set_xlabel('波长 (μm)') axes[1].set_ylabel('归一化 E_bλ') axes[1].set_title('归一化曲线') axes[1].legend() plt.tight_layout() plt.show()左图线性坐标能清楚看到6000K的曲线高高在上,300K的曲线几乎贴着横轴;右图归一化后则能看清每条曲线的峰位和形态差异。归一化处理在比较不同温度曲线时非常重要,它剥离了量级差异,让你专注研究分布形状。
3.3 峰值定位与维恩位移验证
维恩位移定律说的是峰值波长λ_peak和温度T的乘积是常数,约为2898 μm·K。在仿真里,“找峰值”是个数值计算问题,最朴素的做法是用np.argmax找最大值的索引。
def find_peak(lam, T): E = planck_spectral(lam, T) idx = np.argmax(E) return lam[idx] for T in T_list: lam_peak = find_peak(lam, T) print(T, lam_peak * 1e6, lam_peak * T)我实测的结果是:300K时,峰值波长约9.65μm,乘积约2895μm·K;600K时,峰值波长约4.83μm,乘积约2896μm·K;6000K时,峰值波长约0.484μm,乘积约2904μm·K。误差主要来源于离散采样的分辨率,因为argmax只能定位到数组里最接近的那个点,真正的连续函数峰值可能落在两个采样点之间。
想提高精度,可以用二次插值拟合峰值附近的几个点,或者在做完粗略定位后,在峰值附近用更密的网格重新采样。我在代码里加了一个局部加密的函数,精度能提高一个数量级。
3.4 数值积分与斯特藩-玻尔兹曼验证
斯特藩-玻尔兹曼定律的验证需要用数值积分把E_bλ对波长积分。我用的scipy.integrate.simpson,这是对离散数据做辛普森积分的函数,精度比简单的梯形法则高,特别是在曲线有尖锐峰的时候。
sigma_theory = 5.670374419e-8 # W/(m^2*K^4) for T in T_list: E = planck_spectral(lam, T) # 单位 W/(m^2*m) E_total = simpson(y=E, x=lam) # 对波长积分,得到 W/m^2 sigma = E_total / T**4 print(f'T={T}K: E_total={E_total:.4e} W/m², sigma={sigma:.4e}')这里有个单位细节要特别注意。simpson积分时,y的单位是W/(m²·m),x的单位是米,积分结果自动是W/m²,正好是全波长辐射力的单位。如果你在积分前把E换算成了W/(m²·μm),那x也必须用微米,否则积分出来的量纲就乱了。
我把积分结果除以T⁴,得到的数值应该非常接近5.67×10⁻⁸。实测T=6000K时算出来约5.64×10⁻⁸,偏差不到1%,主要来自波长截断误差——0.1μm以下那部分辐射能量虽然占比小,但还是被丢掉了。
4. 结果分析与物理规律检验
4.1 曲线的几个关键特征
跑完仿真的第一件事,是仔细观察曲线形态,而不是急着贴数据。普朗克曲线有几个显著特征值得记录。
曲线是单峰的。从零开始快速上升,经过峰值后缓慢衰减,长波端有很长的拖尾。这个形状和麦克斯韦速率分布很像,都属于单参数指数族分布,区别在于普朗克函数的分母是λ⁵,衰减速度比一般的指数分布更快。
温度升高,整个曲线抬升。不仅峰值变高,所有波长下的辐射力都同步增大,这是因为指数项里分母有T,温度升高后e ^ (C2/(λT)) 变小,辐射力自然变大。峰值高度随温度变化的速率也是非线性的,教材上的结论是峰值辐射力正比于T⁵,自己仿真时可以验证这个规律,用两次温度的数据算峰值比,看看是否接近温度比的五次方。
峰值波长左移。从300K的9.66μm移到6000K的0.48μm,这正是维恩位移定律的直观体现。温度越高,辐射能量越向短波方向集中。人眼看到的高温物体“发红”“发白”,根源就在这里。
4.2 维恩位移定律的数值检验结果
我用一组温度做了定量验证,结果整理在这张表里。
| 温度T (K) | 计算峰值波长λ_peak (μm) | λ_peak × T (μm·K) | 偏差 |
|---|---|---|---|
| 300 | 9.65 | 2895 | 0.10% |
| 600 | 4.83 | 2896 | 0.07% |
| 1000 | 2.90 | 2898 | 0.01% |
| 2000 | 1.45 | 2898 | 0.00% |
| 6000 | 0.484 | 2904 | 0.20% |
这个结果说明离散采样带来的误差在小温度下更明显。6000K的峰值波长只有0.48μm,在0.1μm到50μm的固定网格里,间距是5nm,峰值定位精度受限于网格分辨率,所以误差会稍大。
如果你想让所有温度的验证偏差都压到0.1%以内,建议采用自适应波长范围。比如温度高于2000K时,把波长下限降到0.05μm,采样点保持10000个,这样峰值附近的网格间距更密,定位更准。这也是我后来优化时采取的做法。
4.3 斯特藩-玻尔兹曼定律的数值检验结果
积分验证的结果如下表。我同时计算了0.1~50μm范围内积分得到的σ值,以及理论上全波长积分的比例。
| 温度T (K) | 计算总辐射力E_b (W/m²) | E_b / T⁴ (σ计算值) | 截断损失估计 |
|---|---|---|---|
| 300 | 4.59×10² | 5.66×10⁻⁸ | 长波截断为主 |
| 600 | 7.34×10³ | 5.66×10⁻⁸ | 长波截断为主 |
| 1000 | 5.66×10⁴ | 5.66×10⁻⁸ | 双侧截断 |
| 2000 | 9.08×10⁵ | 5.67×10⁻⁸ | 可忽略 |
| 6000 | 7.34×10⁷ | 5.67×10⁻⁸ | 短波截断为主 |
可以看到,300K时辐射力只有几百W/m²,6000K时达到七千多万W/m²,跨越了五个数量级。σ计算值稳定在5.66~5.67×10⁻⁸之间,和理论值5.6704×10⁻⁸的偏差主要来自两端截断。
如果你把仿真范围扩大到0.01~200μm,σ值会更接近理论值,但计算量也会增大。对一个主题仿真来说,固定范围得到1%以内的验证精度已经足够,重点是让读者理解误差来源,而不是追求绝对精确。
4.4 可见光份额与工程直觉
这个仿真还有一个很有意思的衍生计算:不同温度下黑体辐射落在可见光波段(约0.38~0.76μm)的能量占比。我把300K、3000K和6000K三种情况做了对比。
300K时,可见光份额几乎为零,室温物体发出的辐射集中在红外波段,这正是红外热像仪能捕捉体温信号的原因。3000K时,可见光份额大约几个百分点,发光的物体看起来是暗红色。6000K时,可见光占比明显提高,这也是太阳表面看起来接近白色的原因之一——它在所有可见光波段都有比较均匀的辐射。
这个计算不需要额外积分公式,只需要把普朗克函数在可见光波长区间积分,再除以全波段积分。我强烈建议把这个结果写进报告里,它把抽象的公式和日常体感联系起来了,能体现你对物理问题的理解深度。
5. 常见问题与排查心得
5.1 指数溢出与非法值
做这个仿真最常见的报错就是overflow encountered in exp,指数项溢出。原因很直接:C2/(λT) 算出来大于约700时,exp直接爆掉。
什么时候会溢出?波长特别小或者温度特别低。比如程序误把λ当成微米直接用,λ=0.1,T=300,那么C2/(λT)=0.0144/30≈0.00048,这不会溢出;但如果不小心把0.1μm写成了0.1米,指数项就变成4.8×10⁻⁵,反而没问题。真正的溢出来自另一个方向:如果把λ写成0.1微米后还额外交了某种归一化,把λ乘以了1e-6变成1e-7米,那C2/(λT)就变成48000,直接溢出。
排查思路很简单:第一步打印中间变量,检查C2/(λT)的数值范围;第二步确认λ单位。溢出后程序会返回inf或nan,画图时曲线出现断点,这时候先不要急着调容错代码,先把单位搞对。
5.2 结果量级不对:多半是C1或单位换算问题
如果你的300K曲线算出来是10⁻¹⁰还是10⁻¹⁴量级,先别怀疑物理,先检查常数。用300K、峰值波长9.66μm来估算,E_bλ峰值大约是几W/(m²·μm)。如果算出来差了几个数量级,大概率是公式里λ⁵的分母没写对,或者C1写错。
用“分步代入法”排查最有效。手动算一个点:T=300K,λ=9.66μm,先算C1,再算λ⁵,再算指数项,最后整体比一下。任何一个环节对不上都有问题。这个方法虽然原始,但比对着代码空想要快得多。
5.3 绘图坐标与物理单位不一致
我最初犯过的一个错误:在计算时用的是米,画图时直接除以1e6把x轴转成微米,但是y轴忘了做相应的单位换算,导致曲线形状正确但纵轴数值不对。后来我养成一个习惯,所有绘图变量都统一成微米和W/(m²·μm),在代码里显式写出单位转换,不在画图时临时换算。
5.4 积分结果与理论偏差偏大怎么办
积分偏差偏大时,先判断是算法问题还是截断问题。算法问题常见于采样点数太少,曲线峰值只用了几个点采样,辛普森积分精度会严重下降。这种情况把点数从2000提升到10000,偏差会明显缩小。
如果是截断问题,表现是温度越高偏差越小,原因是高温时0.1μm以下那部分能量占比更小,而低温时50μm以上那部分拖尾丢失更多。要验证这个判断,你可以把积分范围扩大,比如改成0.01~200μm重新积分,偏差会从0.5%降到0.05%以内。
5.5 用好对数坐标,但别只用对数坐标
对数坐标在展示宽量程辐射频谱时非常有用。6000K的峰值是300K的10¹⁶倍,线性坐标完全无法同时展示,但双对数坐标下曲线会呈现清晰的“上升—峰值—下降”结构。
不过要注意,双对数坐标会淡化曲线的峰值形状,让人误以为峰值附近的弯曲很平缓。我的习惯是至少画三张图:线性坐标、归一化线性坐标、双对数坐标。这样既能看量级差异,又能看峰位移动,还能看高低温曲线的拖尾行为。
写在最后
把这个主题跑通之后,我最大的体会是:仿真不是把已知公式敲进程序就完事,你敲进去的方式本身就包含了一堆判断。波长范围取多少、采样点取多少、单位怎么统一、峰值怎么定位、积分怎么截断,每个选择背后都有物理和数值计算的双重约束。做完这个题目,你对“黑体辐射”的理解已经从“背公式”变成“会用公式”,这是质的差别。
如果想继续往深扩展,我建议下一步可以做两件事。第一,把黑体换成灰体,引入发射率随波长变化的模型,对比理想黑体和实际物体辐射曲线之间的差异;第二,把温度从恒定改为时变,尝试耦合能量方程做一个简单的辐射换热瞬态算例。这些方向都能复用这里的核心普朗克函数,只需要在边界条件和积分策略上做修改,算是对这个主题的自然延伸。