雷达基数据解析:定长记录、编码公式与避坑指南
简介:面向气象数据处理、雷达产品开发与教学科研人员的CINRAD雷达基数据格式参考文档,聚焦SA/SB与CA/CB两类雷达,系统梳理基数据的存储方式、径向排列规则、径向数据长度、距离库长与库数等关键参数,帮助读者在解析二进制文件、编写读取程序或核对数据字段时快速定位依据。压缩包内为1个doc文档,约92KB,按字节顺序与双字节顺序逐项列出雷达信息头、径向数据收集时间、儒略日、不模糊距离、方位角、仰角、体扫仰角数、反射率与多普勒数据的首库距离、库长、库数、各类数据指针、多普勒速度分辨率、VCP模式及Nyquist速度等字段,并给出反射率、速度、谱宽的编码换算公式与无回波、距离模糊等特殊取值说明,同时对比两类雷达在库数与数据区长度上的差异。已有2806人学习,适合需要逐字段查阅、对照编码规则完成数据解码与校验的读者。
1. 雷达基数据:为什么一个 2432 字节的定长记录能装下一整层扫描
拿到一份雷达基数据文件,第一反应往往是「这不就是一堆浮点数吗」。真打开看,前 128 字节里塞着毫秒级时间戳、儒略日、方位角、仰角、库长、库数、三个数据段指针,后面才是按径向排列的观测值。更反直觉的是:每条径向记录长度固定,SA/SB 是 2432 字节,CA/CB 是 4132 字节,不管这一条径向上实际有多少个有效回波,字节数一个不多一个不少。这种「定长记录 + 段内偏移」的设计,是理解整个 CINRAD 基数据格式的钥匙。搞懂它,你才知道为什么读文件时不能按「有多少数据读多少」的思路来,也才能解释后面那些编码公式、指针偏移、无效值标记为什么长成那个样子。
2. 定长径向记录:为什么长度固定反而更省事
2.1 一条径向记录里到底有什么
把一条 SA/SB 径向记录拆开看,结构是分层的。最前面 128 字节是径向头,里面第 15-16 字节是「1-表示雷达数据」的标志位,第 29-32 字节是自 00:00 起的毫秒数,第 33-34 字节是自 1970 年 1 月 1 日算起的儒略日。再往后是方位角、仰角、库长、库数这些几何参数,以及三个关键指针:反射率数据指针、速度数据指针、谱宽数据指针,它们记录的是「偏离雷达数据信息头的字节数」,也就是从第 129 字节开始算的偏移量。
这里有个容易看漏的细节:指针是相对「雷达数据信息头」的偏移,不是相对文件开头的绝对位置。所以真实数据位置 = 128 + 指针值。很多初次解析的人直接拿指针当绝对偏移去 seek,结果读出来的第一个字节永远是错的,这种翻车在解析器开发里太常见了。
径向头之后是数据区。反射率从 129 字节开始,长度由第 55-56 字节的「反射率距离库数」决定,范围 0-460;速度紧接其后,库数由第 57-58 字节给出,范围 0-920;谱宽再往后,库数同样取第 57-58 字节的值。三段数据加起来最多 460 + 920 + 920 = 2300 字节,加上 128 字节头,再补到 2432 字节的固定长度,尾部就是保留区。
2.2 定长设计背后的取舍
为什么不定长?因为雷达是实时流式产出的设备。每个体扫存成一个独立文件,文件里按径向顺序排列,从低仰角到高仰角。如果每条记录变长,写入端要不断计算长度、回填偏移,读取端要先扫一遍才能知道第 N 条径向在哪。定长之后,第 N 条径向的起始位置就是N * 2432,随机访问变成一次乘法,这对需要按仰角、按方位快速定位的可视化程序来说是刚需。
代价是空间浪费。一条径向上如果反射率只有 200 个有效库,剩下 260 个库的位置照样占着字节,填的是无效标记。但雷达数据本身对实时性要求高于存储效率,这个取舍是划算的。我一般会这么理解:这不是为归档设计的格式,是为「边扫边写、边写边读」设计的格式。
2.3 用代码把定长记录读出来
下面这段 Python 演示如何按定长切分并定位三段数据,关键在偏移计算和字节序处理:
import struct RADIAL_LEN = 2432 # SA/SB 单条径向固定长度 HEADER_LEN = 128 # 径向头长度 def parse_radial(buf): # buf 是一条完整的 2432 字节径向记录 # 第 15-16 字节:数据类型标志,1 表示雷达数据 data_flag = struct.unpack_from('<H', buf, 14)[0] # 第 29-32 字节:毫秒时间戳 msec = struct.unpack_from('<I', buf, 28)[0] # 第 37-38 字节:方位角原始值,需按公式换算 az_raw = struct.unpack_from('<H', buf, 36)[0] azimuth = (az_raw / 8.0) * (180.0 / 4096.0) # 第 55-56 字节:反射率库数 n_ref = struct.unpack_from('<H', buf, 54)[0] # 第 65-66 字节:反射率数据指针(相对雷达数据信息头) ptr_ref = struct.unpack_from('<H', buf, 64)[0] # 真实数据起点 = 头长度 + 指针 ref_start = HEADER_LEN + ptr_ref ref_bytes = buf[ref_start:ref_start + n_ref] return { 'flag': data_flag, 'msec': msec, 'azimuth': azimuth, 'n_ref': n_ref, 'ref_raw': list(ref_bytes) }struct.unpack_from('<H', ...)里的<表示小端字节序,这是 CINRAD 基数据的默认约定。az_raw / 8.0 * (180.0 / 4096.0)对应文档里的方位角编码公式,先除以 8 再乘角度分辨率。ref_start = HEADER_LEN + ptr_ref就是前面强调的「指针是相对偏移」的落地写法。如果这里写成ptr_ref直接用,读出来的就是头部的保留字节,表现为反射率全是乱值。
3. 编码公式:为什么观测值要经过一次线性变换
3.1 反射率的 (数值-2)/2-32 从哪来
原始文件里存的不是 dBZ 浮点数,而是一个 0-255 的单字节整数。反射率的解码公式是(数值 - 2) / 2.0 - 32 = DBZ。这个式子拆开看:先减 2,是为了把 0 和 1 两个特殊值让出来——0 表示无回波(低于信噪比阈值),1 表示距离模糊基数据部分。减完之后再除以 2,是把整数刻度映射成 0.5 dBZ 的分辨率;最后减 32,是把零点平移到物理上有意义的区间。
为什么是 0.5 dBZ 而不是 1 dBZ?因为降水回波的动态范围很大,弱回波和强回波可能差几十 dBZ,1 dBZ 的量化误差在弱回波区会显得粗糙。0.5 的步长在单字节 256 个刻度里能覆盖大约 127 dBZ 的范围,足够用。
3.2 速度和谱宽的双分辨率陷阱
速度的编码比反射率多一层分支,取决于第 71-72 字节的「多普勒速度分辨率」:值为 2 表示 0.5 米/秒,值为 4 表示 1.0 米/秒。两种情况下公式不同:
- 分辨率 0.5 米/秒:
(数值 - 2) / 2.0 - 63.5 = 米/秒 - 分辨率 1.0 米/秒:
(数值 - 2) - 127 = 米/秒
谱宽则固定用(数值 - 2) / 2.0 - 63.5 = 米/秒。这里最容易踩的坑是:解析速度时忘了先读分辨率字段,一律按 0.5 的公式算,结果在 1.0 分辨率的文件里速度值整体偏大一倍。这种错误不会报异常,只会让风场图看起来「风速异常偏高」,排查起来很费劲。
def decode_velocity(raw, resolution_flag): if raw in (0, 1): return None # 0/1 为特殊标记,非有效速度 if resolution_flag == 2: # 0.5 m/s 分辨率 return (raw - 2) / 2.0 - 63.5 elif resolution_flag == 4: # 1.0 m/s 分辨率 return (raw - 2) - 127.0 else: raise ValueError('未知速度分辨率标志: %d' % resolution_flag)resolution_flag来自径向头第 71-72 字节,必须在解码每条径向之前读一次。把raw in (0, 1)提前拦掉,是因为这两个值参与公式会算出接近 -64 的假速度,混进风场里就是明显的坏点。
3.3 距离库长与库数:S 波段和 C 波段的分水岭
SA/SB 属于 S 波段,反射率库长 1000 米、最大库数 460;速度和谱宽库长 250 米、最大库数 920。CA/CB 属于 C 波段,反射率库长 500 米、最大库数 800;速度和谱宽库长 125 米、最大库数 1600。这个差异直接决定了程序里数组的维度定义。
文档里给了一个很实在的做法:在头文件里用常量控制。读 S 波段时RGates = 460, VGates = 920, WGates = 920;读 C 波段时改成RGates = 800, VGates = 1600, WGates = 1600。这不是风格问题,是内存布局问题——数组第二维、第三维的大小必须和文件里的库数上限对齐,否则要么越界,要么浪费。
| 参数 | S 波段(SA/SB) | C 波段(CA/CB) |
|---|---|---|
| 径向记录长度 | 2432 字节 | 4132 字节 |
| 反射率库长 | 1000 米 | 500 米 |
| 反射率最大库数 | 460 | 800 |
| 速度/谱宽库长 | 250 米 | 125 米 |
| 速度/谱宽最大库数 | 920 | 1600 |
4. 仰角层与体扫:为什么 11 次 PPI 只算 9 层
4.1 重复仰角的合并逻辑
这是整个格式里最反直觉的一处。以 21 扫描模式为例,VCP 仰角序列是 0.5、0.5、1.5、1.5、2.4、3.4、4.3、6.0、9.9、14.6、19.5,一共 11 次 PPI 扫描。但程序里保存到Elvation[]数组的有效元素只有 9 个:0.5、1.5、2.4、3.4、4.3、6.0、9.9、14.6、19.5。原因在于 SA、SB、CB 雷达在低层每个仰角上扫描两次,程序在存数组时把这两次记为一个仰角层。
为什么低层要扫两次?常见的实现思路是:低仰角受地物遮挡和波束展宽影响大,扫两次可以做时间上的平均或互补,提高近地层数据质量。但对上层应用来说,这两次属于同一层,合并存储更符合「一层一个 PPI」的使用习惯。
这个合并逻辑对解析器的要求是:不能简单地按「径向状态」逐条累加层数,而要按仰角值去重。文档里第 43-44 字节的仰角字段就是去重的依据,第 41-42 字节的径向状态(0 首条、1 中间、2 末条、3 体扫首条、4 体扫末条)则用来判断一条径向在层内的位置。
4.2 三维数组的内存模型
文档给出的存储结构是三维数组:
float VolRef[MaxCuts][MaxRads][RGates]; // 反射率,单位 DBZ float VolVel[MaxCuts][MaxRads][VGates]; // 速度,单位 M/S float VolSpw[MaxCuts][MaxRads][WGates]; // 谱宽,单位 M/S float Elvation[MaxCuts]; // 各层仰角,单位 度MaxCuts = 20是最大层数,MaxRads是方位数(每度保存一个径向,即 360),RGates/VGates/WGates是各要素的库数。这个布局的含义是:第一维是仰角层,第二维是方位,第三维是距离。读取时按「层 → 方位 → 距离」三重循环填充,正好对应雷达扫描的物理顺序。
数组里无效数据标记为-999.0,距离折叠标记为999.0。这两个哨兵值的选择有讲究:-999 远离任何物理合理的 dBZ 或速度值,不会被误判为真实观测;999 作为距离折叠标记,在速度场上是一个明显异常的正值,可视化时一眼能看出来。用哨兵值而不是 NaN,是因为早期 C 代码里 NaN 的比较行为在不同编译器下不一致,哨兵值更稳。
4.3 按方位重采样的必要性
MaxRads定义为「每度保存一个径向」,但实际雷达扫描的径向数不一定正好是 360 的整数倍,方位角也可能不是整度。所以填充数组时需要一个重采样步骤:把每条径向按它的实际方位角映射到最近的整度格点上。如果两条径向落在同一度,通常取先到的或做平均;如果某一度没有径向覆盖,就填 -999.0。
这一步是很多解析器省略掉的,省略的后果是可视化时出现方位向的条纹或空洞。我一般会这么处理:先按round(azimuth)取整度索引,冲突时保留第一条,缺失的格点统一填无效值,保证数组结构完整。
5. 避坑与排查:解析雷达基数据时最常见的五个翻车点
5.1 指针当绝对偏移用,反射率全是乱值
现象:读出来的反射率第一个值总是很大或很小,和实际天气对不上。原因:第 65-66 字节的反射率数据指针是「偏离雷达数据信息头的字节数」,不是文件绝对偏移。解决:真实位置 = 128 + 指针值,代码里显式加上头长度,并在注释里写清楚这个约定。
5.2 忘了读速度分辨率,风速整体翻倍
现象:风场图看起来风速异常偏高,但形态合理。原因:速度解码公式依赖第 71-72 字节的分辨率标志,2 对应 0.5 米/秒、4 对应 1.0 米/秒,漏读后一律按 0.5 算,1.0 分辨率的文件速度值偏大一倍。解决:每条径向解码前先读分辨率字段,用分支函数处理,遇到非 2 非 4 的值直接抛异常而不是猜。
5.3 按径向条数累加层数,层数对不上
现象:程序报告的仰角层数是 11,但实际有效层只有 9。原因:低层重复仰角被当成两层累加。解决:按仰角字段去重,而不是按径向状态计数;用Elvation[]数组存去重后的仰角值,填充时判断当前仰角是否已存在。
5.4 字节序搞反,时间戳变成天文数字
现象:毫秒时间戳解出来是几亿甚至几十亿,儒略日完全不对。原因:CINRAD 基数据默认小端字节序,用大端解析会把高低字节颠倒。解决:所有struct.unpack统一加<前缀,或者在读取前用文件头第 15-16 字节的标志位做一次自检——正常值应该是 1,如果解出来是 256,说明字节序反了。
5.5 数组维度按 S 波段写死,读 C 波段越界
现象:读 C 波段文件时程序崩溃或数据错位。原因:RGates/VGates/WGates常量按 S 波段定义(460/920/920),而 C 波段是 800/1600/1600,第三维不够大导致越界。解决:把库数常量做成可配置,读文件前先根据文件长度或波段标识选择常量组,不要全局写死。
提示:排查时优先验证三件事——字节序、指针偏移基准、速度分辨率标志。这三处错了,后面所有解码都是错的,而且不会报错,只会静默产出坏数据。
6. 从格式约束反推设计:定长、哨兵值与指针偏移的取舍
回头看这套格式,几个设计选择其实是互相咬合的。定长径向记录让随机访问变成乘法,代价是尾部保留区浪费空间;指针偏移让三段数据可以不等长地塞进定长记录,代价是解析时必须做一次加法;哨兵值 -999.0 和 999.0 让无效数据和距离折叠在浮点数组里可区分,代价是下游算法要显式过滤这两个值,不能直接当数值参与统计。
如果让我重新设计一个类似的格式,我可能会把指针改成绝对偏移,减少一层加法的心智负担;也可能把无效标记换成 NaN,让浮点运算自然传播。但站在当时的工程约束下,定长加相对偏移加哨兵值的组合,是在实时写入、随机读取、C 语言数组模型三者之间找到的平衡点。理解这个平衡,比记住每个字节的含义更重要——因为字节含义会随波段和版本变,而「为什么这么排」的逻辑是稳定的。
验证一个解析器是否正确,我习惯用两个自检:一是把解出的方位角序列按顺序打印,应该单调递增并在 360 度处回绕;二是把反射率数组的无效值比例统计出来,正常体扫里无效值占比应该在一个合理区间,如果接近 100% 或 0%,基本可以断定偏移或字节序出了问题。这两个检查不需要真实天气做参照,纯靠格式自身的约束就能暴露大部分错误。
从那以后我每次写新的基数据解析器,都会先把「字节序、指针基准、分辨率标志」这三项做成启动时的断言,跑不通就不往下走。希望帮到你。
本文还有配套的精品资源,点击获取