简介:这是一份基于HITRAN数据库的Python光谱处理库hapi源代码,面向分子光谱分析、大气遥感、环境监测及天体物理学研究等领域的科研人员、学生和Python开发者。hapi库封装了访问HITRAN数据库的接口,能够获取分子吸收谱线的频率、强度、压力展宽等关键参数,并支持光谱仿真与数据建模,可帮助用户快速检索谱线数据、模拟气体吸收特性、解析观测光谱。压缩包内容集中,共1个文件,为hapi.py主程序源代码,包体大小约327KB,导入后即可调用数据查询、光谱计算等核心函数,便于直接使用或二次开发。该资源目前已有630人浏览学习,具有一定参考热度。通过研读源码,读者可以完整了解HITRAN数据库的访问机制与光谱仿真实现思路,也能根据自身需求修改和扩展功能,将其嵌入科研或工程流程,显著提升光谱分析和建模效率,是学习分子光谱编程实现的实用参考。
1. HITRAN 数据库和 HAPI:为什么 Python 光谱计算先从这里入手
做激光吸收光谱测量的人,手里都有一本绕不开的黑皮书:HITRAN。它是分子吸收谱线的权威数据库,过去想算一条吸收曲线,得先下载 .par 文件,再用 Fortran 或 C 写读表程序,最后自己拼 Voigt 卷积。HAPI 把这个链路压成了一个 Python 接口:import hapi 之后,拉数据、查表、算吸收系数、出透射谱,几行代码就能跑完。下面沿着我做 TDLAS 气体检测时的路径讲清楚:HITRAN 数据库怎么被 HAPI 用起来,环境参数怎么设,混气浓度怎么乘,以及那些让你算完对不上实验的坑。适合正在做红外气体传感、大气遥感或燃烧诊断,需要快速拿到可复现光谱的工程师和学生。
2. 用 HAPI 读 HITRAN 的第一次计算:数据目录、fetch 与吸收系数
2.1 HITRAN 谱线文件的结构:一行为一条谱线
HAPI 读进去的 HITRAN 数据,本质上是一张扁平的谱线表,每行代表一条跃迁谱线。HITRAN 老版本用固定 100 字符的行,新版扩展到 160 字符,核心字段没有大变。了解这些字段不是为了手写解析器——HAPI 已经全做了——而是为了后面排查问题:为什么拉到的数据量不对、为什么线宽算出来很怪、为什么温度依赖指数会让仿真和实验对不上。
HITRAN 一条谱线的主要字段如下:
| 字段 | 单位 | 作用 |
|---|---|---|
| 真空波数 | cm⁻¹ | 吸收峰位置 |
| 线强度 S | cm⁻¹/(molecule·cm⁻²) | 296 K 下该跃迁的积分强度 |
| 爱因斯坦 A 系数 | s⁻¹ | 自发辐射概率,辐射计算用 |
| 空气增宽半宽 | cm⁻¹/atm | 由空气碰撞导致的 Lorentz 半宽 |
| 自增宽半宽 | cm⁻¹/atm | 同种分子碰撞贡献 |
| 低态能量 E″ | cm⁻¹ | 决定线强度随温度的变化 |
| 温度依赖指数 n | 无量纲 | 展宽随温度变化的幂指数 |
| 压力位移 | cm⁻¹/atm | 高压下峰位移动 |
注意,HITRAN 给的都是单条谱线的参数,不是连续谱。要得到一条连续的吸收光谱,必须把若干条 Voigt 轮廓叠加起来,并考虑温度和压力对宽度的影响。HAPI 的absorptionCoefficient_Voigt做的就是这件事,但你得先把目标谱线拉到本地:
import hapi # 初始化本地数据目录,之后 fetch 的数据会存到这里 hapi.db_begin('hitran_local') # 拉取 CO2 两个主要同位素在 4100~4200 cm-1 的谱线 hapi.fetch('CO2', 1, 2, 4100, 4200) # 拉完之后将它读成 pandas DataFrame,方便预览和筛选 df = hapi.table('CO2') print(df.shape) print(df.head())这里db_begin的参数是本地路径字符串,整个脚本只执行一次即可;fetch的第一个参数是 HITRAN 内部约定的分子名,第二、三个参数是同位素序号范围,第四、五个是波数范围,单位是 cm⁻¹;table返回一个 DataFrame,后面所有筛选、存档都以它为起点。如果fetch时连接失败,程序会直接报错,可以先到 HITRANonline 网页确认该分子在这个波段确实有可下载的谱线再重试。
2.2 第一次算出吸收系数:Environment 和 Diluent 两个参数
谱线拉到之后,计算吸收系数是 HAPI 最核心的一步。常见做法是先生成等间距波数网格,再调用吸收系数函数:
# 生成等间距波数网格:范围 4100~4200 cm-1,步长 0.01 cm-1 grid = hapi.linear_wavenumber_grid(4100, 4200, 0.01) # 计算 CO2 在 296 K、1 atm 下的吸收系数 nu, coef = hapi.absorptionCoefficient_Voigt( 'CO2', grid, hapi.Environment(296, 101325), # 温度 K,压力 Pa hapi.Diluent('air') # 空气增宽背景 ) print(len(nu), nu[0], nu[-1]) print(coef.max())grid是一维数组,范围和端点都包含;步长 0.01 cm⁻¹ 在 4100~4200 之间就是 10001 个点,对第一次跑通来说足够。Environment构造时温度用开尔文,压力用帕斯卡,101325 是 1 atm,这也是后面最容易踩的单位坑。Diluent('air')表示谱线的碰撞展宽由空气分子贡献,适用于大气或烟气场景;如果你在做高纯样品池,就改成hapi.Diluent('self')用自增宽。coef的物理意义是纯气体的吸收系数,单位是 cm⁻¹,这个细节到第三章算混气透射率时会再次出现。
选 4100~4200 cm⁻¹ 这个窗口不是随意的:CO₂ 在 4.3 μm 附近有强吸收带,这个区间谱线密度适中,拉数据快,计算也不会因为谱线数量太多而卡住。第一次跑通后,你可以换成 H₂O 的 6800~6900 cm⁻¹ 窗口,那是水汽近红外检测的常用区域,谱线强且彼此分开,适合观察单条线型。
2.3 别急着画图:先看 describe 和列名
很多新手拉完数据直接plot(nu, coef),画出来发现不对又来群里问,其实 10 秒钟的检查就能避免。我一般先看 DataFrame 的统计信息和半宽相关列名:
print(df.describe()) print([c for c in df.columns if 'gamma' in c.lower()])HAPI 不同版本的列名略有差异,gamma相关列通常是空气/自增宽半宽。先describe能快速确认数量级正常,比如线强度列不是全 0、波数范围落在目标区间内;再检查coef.max(),如果它是 0,多半是fetch没拉到数据,或者拉到的数据波数范围和你的网格没交集。这一步是 10 秒的后悔药,比后面调半天横坐标强得多。数据确认没问题后,建议先把 DataFrame 存成 pickle 或 csv,后面反复计算就不用每次联网拉取了。
提示:HITRAN 的波数值是真空波数,和空气中的波长换算时要考虑空气折射率。在近红外波段,这个偏移通常只影响小数点后第四位,做一般仿真可以忽略,但高精度波长标定时别忽略。
3. 温度压力与 Voigt 线型:把单条谱线变成一张可用的吸收光谱
3.1 为什么必须用 Voigt 线型:黑匣子里面的两条展宽路径
即使 HITRAN 把谱线参数给全了,吸收光谱也不是在谱线位置插一根细线就完事。分子在气体里不是静止的:微观速度分布让吸收频率发生多普勒移动,单条谱线被抹成高斯形,宽度正比于开尔文温度的平方根;同时分子间碰撞打断能级寿命,产生 Lorentz 形,宽度近似正比于压力和碰撞截面。真实气体里两种机制同时存在,理论线型是两者的卷积,也就是 Voigt。低压下 Voigt 趋近高斯,高压下趋近 Lorentz,中间区域没有简单的解析表达式,只能按数值卷积算。
HAPI 的absorptionCoefficient_Voigt把这个黑匣子封装好了:传入网格、Environment和Diluent,它逐条谱线计算 Voigt 轮廓再累加。只算一条线的时候看不出它的价值,算几百条线叠出来的带谱时,手写卷积非常容易翻车。理解这个背后的机制仍然有用:当你发现仿真谱线型不对,第一个要判断的就是当前压力处于哪个展宽主导区。低压气体池三四千帕的时候多普勒展宽占主导,HAPI 算出来的是接近高斯的光滑峰;常压烟气测量时碰撞展宽占主导,峰形更宽更矮,左右不对称的概率也更高。
3.2 用两组环境对比:压力怎么改变峰的“高矮胖瘦”
一次计算看不出问题,两组环境参数放一起对比,Voigt 的行为就非常直观:
import numpy as np grid = hapi.linear_wavenumber_grid(4100, 4200, 0.01) # 同一温度,两个压力:50 kPa 和 101.325 kPa _, coef_50kpa = hapi.absorptionCoefficient_Voigt( 'CO2', grid, hapi.Environment(296, 50000), hapi.Diluent('air')) _, coef_1atm = hapi.absorptionCoefficient_Voigt( 'CO2', grid, hapi.Environment(296, 101325), hapi.Diluent('air')) # 峰值对比:低压峰更高更窄,高压峰更低更宽 i50, i1 = np.argmax(coef_50kpa), np.argmax(coef_1atm) print('峰值位置', grid[i50], grid[i1]) print('峰值系数', coef_50kpa[i50], coef_1atm[i1]) # 积分面积应接近一致:压力只改形状,不改总强度 area_50 = np.trapz(coef_50kpa, grid) area_1 = np.trapz(coef_1atm, grid) print('面积比', area_50 / area_1)变量名前加下划线是表示丢弃第一个返回值,因为两次调用里的nu和grid完全一样。理论上这两个area应该非常接近,因为谱线积分强度不变,压力改变的只是能量在频率上的分布方式。如果面积比明显偏离 1,比如差出 30%,基本可以断定压力单位或温度参数有问题,先回头检查Environment再继续。这个“尖峰对矮宽峰”的对比也是判断计算有没有入误区的最快方法。实际测量里,低压气体池抽到几千帕正是为了让吸收峰变尖、避免邻近线重叠,TDLAS 选线时经常利用这个趋势。
3.3 从吸收系数到透射率:Beer-Lambert 与浓度乘法的漏网之鱼
HAPI 算出来的coef默认对应纯气体。Beer-Lambert 定律写出来是 I/I₀ = exp(-k·L),k 是吸收系数,L 是光程长度。但如果你的气体池里只有 5% CO₂、其余是氮气,就不能直接把coef代进去。我一般先把coef乘待测组分的体积分数 x,再算指数:
L = 100 # 光程 100 cm x_co2 = 0.05 # 混合气里 CO2 占 5% trans = np.exp(-coef * x_co2 * L) # 找最深吸收位置(透射率最低处) idx = np.argmin(trans) print(grid[idx], trans[idx])x_co2可以是 0.05,也可以是 400e-6(400 ppm),看你模拟的对象。如果光路是开放大气,x 是沿程体积分数,L 是整条光路长度,算出来的是路径积分透射率;如果要进一步做开放光路浓度反演,还得把温度压力梯度考虑进来,HAPI 的Environment描述的是均匀气体池,别拿它硬套非均匀光路。HAPI 本身也提供透射率和辐射率的封装函数,但我更习惯直接用 numpy 算透射率,少记一层函数签名,也方便对数组做各种后续处理,比如插值到仪器波长网格。
4. HAPI 排查与避坑:五个让光谱计算翻车的现场
这一章写的都是我在实际项目里见过或踩过的坑,每条按“现象 → 原因 → 解决”整理。如果你照着第三章的代码跑出来和实验对不上,先按这五条排查,命中率很高。
4.1 fetch 返回空:先确认波段有谱线,再检查本地目录
现象:fetch执行完没有报错,但df.shape是 (0, N);或者执行时报 HTTP 错误,程序中断。
原因:第一种多半是选错了分子或波数范围,比如 CO 在 4100~4200 cm⁻¹ 只有很少弱线,强度低于 HITRANonline 的下载阈值;第二种情况是本地目录没写权限,或者网络连接失败。还有一种更隐蔽的场景:db_begin指向了之前建过的目录,里面已经有旧数据,fetch拉的是增量,但拉完后的表和新旧数据混在一起,行数看着对不上。
解决:先到 HITRANonline 网页按分子和波数范围搜索,确认该波段存在可下载的谱线;再看db_begin目录是否有写入权限;最后把目录换成新路径重跑一次,排除旧缓存干扰。数据拉到后顺手df.to_pickle('co2_band.pkl')存档,后面每次计算直接读本地文件,不再依赖网络。
4.2 峰位对不上:HITRAN 的波数和光学仪器的波长是两个单位
现象:代码里网格在 6500 附近,画图却找不到 1.5 μm 附近的吸收峰,或者峰位刚好左右反过来。
原因:HITRAN 所有谱线位置都用真空波数 cm⁻¹,而很多仪器和文献显示波长 nm。6500 cm⁻¹ 对应约 1538 nm,如果直接把 6500 当 nm 去和仪器对照,自然全部错位。
解决:用换算公式wavelength_nm = 1e7 / nu,numpy 数组可以直接这样除:
wavelength_nm = 1e7 / nu注意波数从大到小对应波长从小到大,画图时横坐标顺序和仪器扫描方向经常是反的。我踩过这个坑之后养成了一个习惯:每次算完先打印峰值点的波数和波长,和已知谱线位置对一眼再画图,而不是直接丢进绘图函数。
4.3 峰宽差几个数量级:Environment 的压力单位不是 atm
现象:算出的谱线比实测窄很多,峰高却高得离谱,看起来像一根针插在背景上。
原因:Environment的压力单位是 Pa,但有人脑子里想着 atm,直接在第二个参数里填 1。于是碰撞展宽被低估了 101325 倍,Voigt 线型里 Lorentz 部分几乎消失,谱线退化成纯多普勒线。
解决:把 1 改成 101325。如果你从别处拿到以 atm 为单位的数据,确认单位后统一乘 101325 再传。顺带也要检查半宽参数的单位,HITRAN 的空气增宽半宽是 cm⁻¹/atm,HAPI 内部会按压力单位处理,但你自己手写展宽公式时特别容易在单位换算里迷糊。
4.4 网格太密导致计算卡死:先粗扫再细扫
现象:网格步长设 0.001,波数范围 4000~4500,跑了几分钟没结果,内存肉眼可见地涨。
原因:50 万个波数点,叠加几百条谱线,每个点都要做 Voigt 卷积,这个计算量不是普通 Python 循环能扛住的。HAPI 内部虽然有优化,但网格规模和谱线数量一起来,照样会卡。
解决:分两步走。先用 0.05 或 0.1 的步长跑完整波段,确认主要吸收线的位置,再对每条目标线做 ±0.5 cm⁻¹ 的小窗口、0.001 步长细扫。fetch范围也按窗口来,不要在第一步就一把拉完几千个波数。如果项目需要快速迭代,建议只保留线强度大于 1e-23 的谱线参与计算,弱线对宽谱带的贡献通常可以忽略。
4.5 算出的吸收太强:直接 exp(-coef·L) 是纯气体的透射率
现象:同一浓度下仿真吸收比实测大很多,或者标定时怎么补偿都补偿不过来。
原因:就是第三章说的那个乘法漏了。HAPI 的Environment只有温度和总压,没有组分分压,coef对应的是纯气体。烟气里 CO₂ 通常只有百分之几,水汽往往只有 1%~3%,直接代进去等于把整个光路当成了纯 CO₂ 池子。
解决:算透射率前先乘体积分数:trans = np.exp(-coef * x * L)。如果做痕量气体检测,x 在 ppb 量级,透射率非常接近 1,这是正常的,后期再用拟合算法把 x 当未知数反演。比这个更隐蔽的是数据版本问题:HAPI 默认拉的是当前 HITRANonline 版本,和某些论文里用的 HITRAN 2012 版数据在弱线上可能差几个百分点,做高精度对比前先把fetch得到的版本元信息记录下来。
5. 验证你的 HAPI 光谱:一个从 HITRAN 到 npz 的最小闭环
5.1 从 HITRAN 到 npz:一个能直接跑的最小验证脚本
最后一个章节给一个可复用的验证脚本,选水汽 6800~6900 cm⁻¹ 窗口,约 1.45~1.47 μm,是 TDLAS 测湿度的常用区域。脚本把拉数据、算吸收系数、算透射率、存盘放在一起,跑完得到一个 npz 文件,后续校准和反演直接读它:
import numpy as np import hapi # 独立目录:路径不同就不会被旧数据污染 hapi.db_begin('hitran_h2o_demo') hapi.fetch('H2O', 1, 1, 6800, 6900) grid = hapi.linear_wavenumber_grid(6800, 6900, 0.01) nu, coef = hapi.absorptionCoefficient_Voigt( 'H2O', grid, hapi.Environment(296, 101325), hapi.Diluent('air')) # 积分面积守恒检查:压力只改形状,不应明显改变面积 area = np.trapz(coef, nu) print('吸收系数面积:', area) # 400 ppm 水汽、100 cm 光程的透射率 trans = np.exp(-coef * 400e-6 * 100) # 存盘,后续校准和拟合直接读 np.savez('h2o_1450nm.npz', nu=nu, coef=coef, trans=trans)np.trapz是老版本 numpy 通用的写法,新环境如果提示弃用,改成np.trapezoid即可。这个面积值本身没有绝对标准,更好的验证是和df里的线强度总量对比:吸收系数对波数的积分约等于该窗口内所有谱线强度贡献之和,数量级一致就说明单位没搞错。
5.2 怎么确认结果没算错:先打一个点,再画整条线
我每次新写一个光谱计算脚本,都会先打印峰值位置和峰值透射率,再把数组头和尾各打三个点,看边界是不是干净的 1.0 透射率。如果边界出现负值或明显突变,多半是网格范围没有覆盖所有谱线的尾部,Voigt 轮廓被截断了。这时候加宽波数范围,而不是加密步长,通常就能解决。再拿这个仿真谱和 HITRANonline 网页的快速浏览谱叠一张图,量级一致就可以放心继续做反演。最后说一条我的血泪教训:最早用 HAPI 算水汽谱,我把波数当成了波长,整套曲线横坐标反着画,峰值对到了一条完全不同的谱线,之后半天都在查空气池还是标定问题。后来无论多急,第一件事永远都是打印峰值点的波数和波长,对上了再画图。希望这个验证习惯也能帮到你。
本文还有配套的精品资源,点击获取