简介:这份资源面向大气科学、遥感与气象观测方向的学习者和科研人员,围绕CE318型太阳光度计的观测数据,提供从原始数据读取到气溶胶光学厚度(AOD)与水汽含量(WV)反演的完整处理思路。内容涉及多波段辐射数据清洗、异常值与缺失值处理、Klett法与Fernald法等气溶胶光学反演算法,以及940nm水汽吸收带的反演流程,并兼顾温度、气压、相对湿度等气象因素对辐射传输的影响,适合具备一定大气光学与编程基础、希望动手实践的中高级读者。资源包共5个文件,均为cpp源码,压缩后约9KB,涵盖数据读取、AOD对比、水汽对比及定标等核心模块,结构紧凑、便于直接编译调试。目前已有1082人学习下载,可作为搭建反演流程、理解算法实现与排错验证的实用参考。
1. CE318 数据反演 AOD 与 wv:从原始文件到可交付产品的完整链路
如果你手头有一台 CE318 太阳光度计,或者拿到了一批历史观测数据,却卡在“原始文件怎么变成 AOD 和大气水汽柱含量”这一步,那这篇笔记就是为你写的。CE318 是法国 CIMEL 公司生产的多波段太阳光度计,全球气溶胶观测网络里大量使用它作为标准仪器。它的原始数据是 .dat 或 .k7 格式,里面记录的是各波段在不同天顶角下的太阳辐照度原始计数值,并不是直接可用的 AOD。从这些计数值到最终的大气气溶胶光学厚度(AOD)和大气水汽柱含量(wv),中间要经过定标系数应用、瑞利散射扣除、气体吸收修正、云筛查、Langley 定标或使用官方定标系数等一系列步骤。很多刚接触的人以为拿到数据就能直接算,结果第一步读文件就翻车了。这篇内容会按“数据格式解析 → 定标与反演原理 → 代码实现 → 避坑排查 → 进阶技巧”的顺序,把整条链路拆开,让你能照着复现出一套可用的处理流程。
2. CE318 原始数据格式解析与预处理:从 .dat 到结构化表格
2.1 CE318 数据文件里到底存了什么
CE318 的原始数据文件通常按天存储,文件名里包含站点编号和日期信息。文件内部是固定格式的文本行,每一行对应一次观测。一次观测记录里包含:观测时间(UTC)、太阳天顶角、各波段(1020nm、870nm、670nm、440nm、380nm 等)的原始计数值、以及 940nm 水汽通道的计数值。不同固件版本和不同网络(如 AERONET、CARSNET)的数据格式略有差异,但核心字段是一致的。
常见做法是先用 Python 把原始文件读成 pandas DataFrame,再做后续计算。我一般会写一个解析函数,按行读取、按固定列宽或分隔符拆分。注意:有些文件用空格分隔,有些用制表符,还有些在头部有若干行说明文字,需要先跳过。
import pandas as pd import numpy as np from datetime import datetime def parse_ce318_dat(filepath): """ 解析 CE318 原始 .dat 文件,返回结构化 DataFrame 参数 filepath: 原始数据文件路径 返回: 包含时间、天顶角、各波段计数值的 DataFrame """ records = [] with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: for line in f: line = line.strip() # 跳过空行和头部说明行 if not line or line.startswith('#'): continue parts = line.split() # 典型行长度至少包含时间、天顶角、5个波段计数值 if len(parts) < 10: continue try: # 假设前两列是日期和时间,第三列是天顶角 date_str = parts[0] time_str = parts[1] zenith = float(parts[2]) # 后续列为各波段原始计数值,顺序按仪器配置 counts = [float(x) for x in parts[3:10]] records.append({ 'datetime': datetime.strptime(date_str + ' ' + time_str, '%Y-%m-%d %H:%M:%S'), 'zenith': zenith, 'counts_1020': counts[0], 'counts_870': counts[1], 'counts_670': counts[2], 'counts_440': counts[3], 'counts_380': counts[4], 'counts_940': counts[5], 'counts_870_2': counts[6] # 部分仪器有重复波段 }) except (ValueError, IndexError): continue df = pd.DataFrame(records) df = df.sort_values('datetime').reset_index(drop=True) return df这段代码的逻辑是:逐行读取,跳过注释和空行,按空白字符拆分,提取时间、天顶角和计数值。参数说明:filepath是原始文件路径;返回的 DataFrame 里counts_*列是原始计数值,后续要乘以定标系数才能得到辐照度。注意不同站点的列顺序可能不同,实际使用时要根据数据说明文档调整索引。
2.2 定标系数从哪来、怎么用
CE318 的原始计数值必须乘以定标系数才能转换为大气层顶的太阳辐照度。定标系数通常由官方定标中心提供,文件里包含每个波段的斜率(slope)和截距(intercept),或者直接给出定标系数。常见做法是:从定标文件里读取每个波段的系数,然后对原始计数值做线性变换。
def apply_calibration(df, calib_dict): """ 应用定标系数,将原始计数值转换为辐照度 参数 df: 原始 DataFrame 参数 calib_dict: 字典,格式为 {'1020': (slope, intercept), ...} 返回: 添加了辐照度列的 DataFrame """ for band, (slope, intercept) in calib_dict.items(): col_name = f'counts_{band}' if col_name in df.columns: # 辐照度 = 计数值 * 斜率 + 截距 df[f'radiance_{band}'] = df[col_name] * slope + intercept return df这里的关键参数是slope和intercept,它们来自定标文件。如果没有官方定标系数,可以用 Langley 定标法从数据本身反推,但那是另一个话题,后面会提。注意:定标系数有有效期,通常每年更新一次,用错年份的系数会导致 AOD 系统性偏差。
3. AOD 反演核心:瑞利散射扣除与气体吸收修正
3.1 从辐照度到 AOD 的计算公式
AOD 的反演基于 Beer-Lambert-Bouguer 定律。简单说,仪器测到的太阳辐照度经过大气衰减后,满足:
I = I0 * exp(-m * τ)
其中 I 是地面测到的辐照度,I0 是大气层顶辐照度(由定标得到),m 是大气质量数(与太阳天顶角有关),τ 是总光学厚度。总光学厚度包含瑞利散射、气溶胶散射和气体吸收。我们要的是气溶胶部分,所以需要把瑞利散射和气体吸收扣掉。
def compute_aod(df, calib_dict, wavelengths=[1020, 870, 670, 440, 380]): """ 计算各波段 AOD 参数 df: 已应用定标的 DataFrame 参数 calib_dict: 定标系数字典 参数 wavelengths: 要计算的波段列表 返回: 添加了 AOD 列的 DataFrame """ # 瑞利散射光学厚度近似公式(海平面标准大气) def rayleigh_od(wl_nm): # wl_nm 为波长,单位纳米 return 0.008569 * (wl_nm / 1000.0) ** (-4) * (1 + 0.0113 * (wl_nm / 1000.0) ** (-2) + 0.00013 * (wl_nm / 1000.0) ** (-4)) # 臭氧吸收光学厚度(简化处理,实际需根据臭氧柱含量调整) def ozone_od(wl_nm, ozone_du=300): # 臭氧吸收系数近似 k_ozone = {1020: 0.0, 870: 0.0, 670: 0.02, 440: 0.03, 380: 0.01} return k_ozone.get(wl_nm, 0.0) * (ozone_du / 300.0) for wl in wavelengths: rad_col = f'radiance_{wl}' if rad_col not in df.columns: continue # 计算大气质量数,使用 Kasten-Young 公式 zenith_rad = np.radians(df['zenith']) df['airmass'] = 1.0 / (np.cos(zenith_rad) + 0.15 * (93.885 - df['zenith']) ** (-1.253)) # 大气层顶辐照度 I0 由定标得到,这里假设 calib_dict 里存了 I0 i0 = calib_dict.get(str(wl), (1.0, 0.0))[0] # 总光学厚度 df[f'total_od_{wl}'] = np.log(i0 / df[rad_col]) / df['airmass'] # 扣除瑞利和臭氧,得到 AOD df[f'aod_{wl}'] = df[f'total_od_{wl}'] - rayleigh_od(wl) - ozone_od(wl) # 过滤负值和异常值 df.loc[df[f'aod_{wl}'] < 0, f'aod_{wl}'] = np.nan df.loc[df[f'aod_{wl}'] > 5, f'aod_{wl}'] = np.nan return df这段代码里,rayleigh_od函数用近似公式算瑞利散射光学厚度,ozone_od用简化系数算臭氧吸收。实际业务中,瑞利散射和臭氧吸收的计算会更精细,比如考虑气压修正和臭氧柱含量的实时变化。参数说明:wavelengths是要计算的波段;airmass用 Kasten-Young 公式算,比简单的 1/cos 更准。注意:如果天顶角大于 80 度,大气质量数会很大,误差也大,通常建议只保留天顶角小于 75 度的观测。
3.2 水汽含量 wv 的反演:940nm 通道的特殊处理
水汽柱含量 wv 的反演用的是 940nm 通道,这个波段位于水汽吸收带内。基本思路是:用 870nm 通道(水汽吸收很弱)作为参考,比较 940nm 和 870nm 的辐照度比值,再通过查找表或经验公式反推水汽含量。
def compute_wv(df, calib_dict): """ 计算大气水汽柱含量 wv 参数 df: 已应用定标的 DataFrame 参数 calib_dict: 定标系数字典,需包含 940 和 870 的定标系数 返回: 添加了 wv 列的 DataFrame """ # 假设已有 radiance_940 和 radiance_870 if 'radiance_940' not in df.columns or 'radiance_870' not in df.columns: return df # 计算透过率比值 # 先算大气质量数 zenith_rad = np.radians(df['zenith']) df['airmass'] = 1.0 / (np.cos(zenith_rad) + 0.15 * (93.885 - df['zenith']) ** (-1.253)) # 940nm 和 870nm 的瑞利散射和臭氧吸收差异很小,可忽略 # 水汽透过率 T_wv = radiance_940 / radiance_870 * (I0_870 / I0_940) i0_940 = calib_dict.get('940', (1.0, 0.0))[0] i0_870 = calib_dict.get('870', (1.0, 0.0))[0] df['t_wv'] = (df['radiance_940'] / df['radiance_870']) * (i0_870 / i0_940) # 根据透过率反推水汽含量,常用经验公式:wv = a * (ln(t_wv))^2 + b * ln(t_wv) + c # 这里用简化系数,实际需根据仪器定标查找表 a, b, c = -0.5, -1.2, 2.0 df['wv'] = a * (np.log(df['t_wv'])) ** 2 + b * np.log(df['t_wv']) + c # 过滤异常值 df.loc[df['wv'] < 0, 'wv'] = np.nan df.loc[df['wv'] > 10, 'wv'] = np.nan return df这里的核心参数是a, b, c,它们来自仪器定标时的查找表拟合。不同仪器的系数不同,用错系数会导致 wv 偏差很大。注意:940nm 通道受温度影响明显,有些处理流程会做温度修正,如果数据里没有温度字段,这一步可以跳过,但精度会打折扣。
4. 避坑与排查:CE318 数据处理里最容易翻车的五个地方
4.1 现象:AOD 出现大量负值 → 原因:定标系数用错或瑞利散射扣除过度 → 解决:核对定标文件年份和波段对应关系
负 AOD 是最常见的翻车现场。我见过有人把 1020nm 的定标系数用到了 440nm 上,结果短波段 AOD 全是负的。排查方法:先检查定标文件里的波段标签和代码里的波段索引是否一致;再检查瑞利散射公式里的波长单位是纳米还是微米,单位搞错会导致瑞利光学厚度差几个数量级。解决方式:把定标系数和波段一一对应打印出来,人工核对一遍。
4.2 现象:wv 值在一天内剧烈跳动 → 原因:940nm 通道信噪比低或云污染 → 解决:加云筛查和滑动平均
940nm 通道的信号比可见光波段弱很多,如果观测时有薄云,wv 会突然跳变。常见做法是:先根据 870nm 的 AOD 变化率做云筛查,如果相邻两次观测的 AOD 差异超过阈值(比如 0.05),就标记为疑似云污染;再对 wv 做时间序列的滑动平均,窗口取 3 到 5 个点。注意:滑动平均会平滑掉真实的水汽变化,所以只建议在数据质量差的时候用。
4.3 现象:天顶角大于 80 度时 AOD 异常偏大 → 原因:大气质量数近似公式在低太阳高度角下失效 → 解决:限制天顶角范围或改用更精确的公式
Kasten-Young 公式在 80 度以上误差会明显增大,而且低太阳高度角时云和地物反射的影响也更大。我一般会直接过滤掉天顶角大于 75 度的观测,除非你专门做晨昏蒙影研究。如果非要保留,可以用更复杂的辐射传输模型算大气质量数,但那样计算量会大很多。
4.4 现象:不同波段算出的 AOD 谱线不平滑 → 原因:气体吸收修正不完整 → 解决:检查臭氧和水汽吸收系数
AOD 随波长的变化应该是一条平滑的曲线(通常随波长减小而增大)。如果 440nm 和 670nm 的 AOD 出现交叉或跳变,很可能是臭氧吸收没扣干净。检查臭氧吸收系数是否用了正确的波长对应值,以及臭氧柱含量是否用了实时数据而不是默认值。水汽吸收在 670nm 附近也有弱吸收带,如果精度要求高,也需要修正。
4.5 现象:Langley 定标结果不稳定 → 原因:数据量不足或大气不稳定 → 解决:选晴朗稳定日,增加观测天数
Langley 定标要求大气光学特性在观测期间保持稳定,通常需要至少半天以上的连续观测,而且天顶角要覆盖足够大的范围。如果数据量少或者当天有云,定标结果会飘。常见做法是:选能见度高、无云的天气,连续观测 3 到 5 天,取定标结果的平均值。注意:Langley 定标对仪器稳定性要求很高,如果仪器本身有漂移,定标结果也不可靠。
5. 进阶技巧:用 Langley 定标反推 I0 与批量处理多日数据
5.1 Langley 定标:没有官方定标系数时的后悔药
如果你拿到的数据没有配套定标文件,或者定标文件过期了,可以用 Langley 定标法从数据本身反推大气层顶辐照度 I0。原理是:在稳定大气条件下,总光学厚度与大气质量数呈线性关系,把不同天顶角下的 ln(辐照度) 对大气质量数做线性回归,截距就是 ln(I0)。
def langley_calibration(df, band='870'): """ 用 Langley 法反推 I0 参数 df: 包含 radiance_{band} 和 airmass 的 DataFrame 参数 band: 波段字符串 返回: I0 估计值 """ rad_col = f'radiance_{band}' if rad_col not in df.columns or 'airmass' not in df.columns: return None # 只保留天顶角小于 75 度、辐照度大于 0 的观测 mask = (df['zenith'] < 75) & (df[rad_col] > 0) sub = df[mask].copy() if len(sub) < 10: return None # 线性回归:ln(I) = ln(I0) - m * tau x = sub['airmass'].values y = np.log(sub[rad_col].values) # 最小二乘拟合 coeffs = np.polyfit(x, y, 1) ln_i0 = coeffs[1] i0 = np.exp(ln_i0) return i0这段代码的逻辑是:取天顶角小于 75 度的观测,对 ln(辐照度) 和大气质量数做一次多项式拟合,截距就是 ln(I0)。参数说明:band是要定标的波段;返回的i0就是大气层顶辐照度。注意:Langley 定标要求数据来自稳定晴朗日,如果数据里混了云,拟合结果会偏。我一般会先画散点图看一眼,确认线性关系好再拟合。
5.2 批量处理多日数据:把流程串成管道
单日数据处理跑通后,下一步就是批量处理。常见做法是写一个主函数,遍历目录下所有 .dat 文件,依次调用解析、定标、AOD 计算、wv 计算,最后把结果合并成一个 CSV 或 NetCDF 文件。
import os import glob def batch_process(data_dir, calib_dict, output_csv='output.csv'): """ 批量处理目录下所有 CE318 数据文件 参数 data_dir: 数据目录路径 参数 calib_dict: 定标系数字典 参数 output_csv: 输出 CSV 路径 """ all_results = [] for filepath in glob.glob(os.path.join(data_dir, '*.dat')): df = parse_ce318_dat(filepath) if df.empty: continue df = apply_calibration(df, calib_dict) df = compute_aod(df, calib_dict) df = compute_wv(df, calib_dict) all_results.append(df) if all_results: final_df = pd.concat(all_results, ignore_index=True) final_df.to_csv(output_csv, index=False) print(f'处理完成,共 {len(final_df)} 条记录,已保存到 {output_csv}') else: print('没有找到有效数据')这个管道的逻辑很直接:遍历文件、逐个处理、合并输出。参数说明:data_dir是数据目录;calib_dict是定标系数字典;output_csv是输出路径。注意:批量处理时要注意内存,如果数据量很大,可以分批写入而不是全部堆在内存里。另外,不同日期的数据可能来自不同定标周期,如果定标系数有更新,要按日期分段处理。
从那以后我每次拿到新数据,都会先跑一遍单日流程,画个 AOD 和 wv 的时间序列图,确认没有明显异常再批量跑。这个习惯帮我省了很多返工的时间。希望帮到你。
本文还有配套的精品资源,点击获取