简介:面向气象数据处理、雷达产品开发与教学科研人员的CINRAD雷达基数据格式说明文档,聚焦SA/SB与CA/CB两类雷达的二进制基数据组织方式,帮助读者理解原始体扫文件如何解析、字段如何对应,适合需要读取或转换雷达基数据的初中级开发者查阅。压缩包内为1个doc文档,约92KB,以字节顺序表形式逐项列出雷达信息头、径向数据收集时间、儒略日、不模糊距离、方位角、仰角、体扫仰角数、反射率与多普勒数据的距离库长及库数、各类数据指针、VCP模式与Nyquist速度等字段的偏移与编码规则。文档还说明了每个体扫单独存为一个文件、径向数据按仰角由低到高排列、径向长度固定为2432字节,以及反射率库长1000米、速度与谱宽库长250米等关键约定,并给出数值到dBZ、米/秒的换算公式。已有2806人学习,可作为解析雷达基数据的字段对照手册。
1. 拿到一份 CINRAD 基数据却读不出图:这份格式说明能省你三天
前阵子帮某实验室处理一批历史雷达归档,文件后缀是.bin,用现成的解码库一读,反射率整片偏移,速度场直接错位。翻回去查才发现,同一批数据里混了 S 波段和 C 波段两种径向长度——2432 字节和 4132 字节,库数定义没跟着切,数组越界把后面的仰角全冲了。这类问题不是算法难,是格式没吃透。这份《雷达基数据格式说明》干的就是这件事:把 CINRAD SA/SB 与 CA/CB 两类基数据的字节布局、双字节序、编码公式、指针偏移、数组组织一次讲清,适合要做基数据解码、质量控制、拼图或反演,却卡在“字段对不上、数值是乱的”这一步的从业者。它不教你气象原理,只保证你能把二进制原样还原成 dBZ、米每秒和角度。
2. 字节布局拆解:28 字节头加固定径向,先把偏移算对
2.1 两类雷达的径向长度差异从哪来
SA/SB 属于 S 波段,径向固定 2432 字节;CA/CB 属于 C 波段,径向固定 4132 字节。这个长度不是随便定的,它由“头 + 反射率库 + 速度库 + 谱宽库 + 保留”拼出来。S 波段反射率最多 460 库、速度和谱宽各 920 库;C 波段反射率 800 库、速度和谱宽各 1600 库。库数翻倍,径向自然变长。
真正容易翻车的是:文件里每个径向都带一份 128 字节的径向头(前 14 字节保留,15-16 是数据类型标志,17-28 保留,29 开始才是有效字段),而数据区起点不是死数,是由头里的三个指针字段给出的。很多人图省事直接写offset = 128去读反射率,遇到某些体扫就偏了。正确做法是先读指针,再定位。
| 字段 | SA/SB 字节位置 | CA/CB 字节位置 | 说明 |
|---|---|---|---|
| 数据类型标志 | 15-16 | 15-16 | 1 表示雷达数据 |
| 收集时间(ms) | 29-32 | 29-32 | 自 00:00 起算 |
| 儒略日 | 33-34 | 33-34 | 自 1970-01-01 起 |
| 不模糊距离 | 35-36 | 35-36 | 数值/10 = 千米 |
| 方位角 | 37-38 | 37-38 | [值/8]×[180/4096] 度 |
| 仰角 | 43-44 | 43-44 | 同上编码 |
| 反射率指针 | 65-66 | 65-66 | 偏离径向头起点的字节数 |
| 速度指针 | 67-68 | 67-68 | 同上 |
| 谱宽指针 | 69-70 | 69-70 | 同上 |
| 速度分辨率 | 71-72 | 71-72 | 2=0.5m/s,4=1.0m/s |
| VCP 模式 | 73-74 | 73-74 | 11/21/31/32 |
注意表里“字节位置”是相对单条径向起点算的,不是相对整个文件。整文件是按径向一条条顺序排的,所以读第 n 条径向,文件偏移是n × 径向长度,再在这个基址上加表内偏移。
2.2 双字节序:小端读错就是天文数字
格式里所有 2 字节整数字段都是“双字节顺序”,实际落地就是小端(低字节在前)。用 C 直接fread到unsigned short在 x86 上没问题,但用 Python 的struct必须显式写<H,写成>H会得到几万的方位角。方位角编码是[数值/8]×[180/4096],一个 16 位值最大 65535,算出来约 360 度,量级是对的;一旦字节序反了,算出来会是 0.0x 度这种明显不合理的值,这就是最快的自检信号。
import struct RADIAL_LEN = {'S': 2432, 'C': 4132} def read_radial_header(buf, band): # buf 为单条径向的 bytes,长度等于 RADIAL_LEN[band] assert len(buf) == RADIAL_LEN[band], "径向长度与波段不匹配" h = {} h['data_type'] = struct.unpack_from('<H', buf, 14)[0] # 15-16 h['msec'] = struct.unpack_from('<I', buf, 28)[0] # 29-32 h['julian'] = struct.unpack_from('<H', buf, 32)[0] # 33-34 h['unamb_km'] = struct.unpack_from('<H', buf, 34)[0] / 10.0 az_raw = struct.unpack_from('<H', buf, 36)[0] # 37-38 h['az_deg'] = (az_raw / 8.0) * (180.0 / 4096.0) el_raw = struct.unpack_from('<H', buf, 42)[0] # 43-44 h['el_deg'] = (el_raw / 8.0) * (180.0 / 4096.0) h['ref_ptr'] = struct.unpack_from('<H', buf, 64)[0] # 65-66 h['vel_ptr'] = struct.unpack_from('<H', buf, 66)[0] h['spw_ptr'] = struct.unpack_from('<H', buf, 68)[0] h['vel_res'] = struct.unpack_from('<H', buf, 70)[0] # 2 或 4 h['vcp'] = struct.unpack_from('<H', buf, 72)[0] return h这段的关键在unpack_from的第三个参数:它是相对buf起点的字节偏移,而格式表里的“字节位置”是从 1 开始数的,所以统一减 1。比如方位角在表里是 37-38,代码里写 36。az_raw / 8.0 * (180.0/4096.0)严格照抄编码公式,不要自己化简成az_raw * 0.005493,化简后精度和可读性都差,排查时对不上原始公式。vel_res读出来是 2 还是 4,直接决定后面速度解码走哪条分支,这个字段漏读,速度场会整体差一倍。
2.3 数据区解码:0 和 1 不是回波
反射率每个库 1 字节,编码(值-2)/2 - 32 = dBZ。这里有两个特殊值必须先拦:值为 0 表示无回波(低于信噪比阈值),值为 1 表示距离模糊基数据部分。如果直接把 0 代进公式,会得到 -33 dBZ,看起来像个弱回波,拼图时就会在晴空区糊上一层假回波。速度同理,0 和 1 意义相同,且速度解码分两档:
- 分辨率 0.5 m/s:
(值-2)/2 - 63.5 - 分辨率 1.0 m/s:
(值-2) - 127
谱宽只有一档:(值-2)/2 - 63.5。数组里无效数据统一记 -999.0,距离折叠记 999.0,这个约定要跟下游的色标和统计逻辑对齐,否则缺测会被当成极端值参与平均。
def decode_reflectivity(buf, h, band): n = 460 if band == 'S' else 800 start = h['ref_ptr'] out = [] for i in range(n): v = buf[start + i] if v == 0: out.append(-999.0) # 无回波 elif v == 1: out.append(999.0) # 距离折叠 else: out.append((v - 2) / 2.0 - 32.0) return out def decode_velocity(buf, h, band): n = 920 if band == 'S' else 1600 start = h['vel_ptr'] out = [] for i in range(n): v = buf[start + i] if v in (0, 1): out.append(-999.0 if v == 0 else 999.0) elif h['vel_res'] == 2: out.append((v - 2) / 2.0 - 63.5) else: out.append((v - 2) - 127.0) return outstart直接用头里的指针,不要假设它等于 128。库数n用波段常量,不要用头里的“距离库数”字段去动态算——那个字段在某些归档文件里填的是实际有效库数,小于最大值,拿它当循环上界会截断远端弱回波。稳妥做法是按最大值读满,超出实际库数的位置自然落在保留区,值通常是 0,会被判成无回波,不影响结果。
3. 从单径向到体扫数组:仰角层合并与内存组织
3.1 为什么 11 个 PPI 只存 9 层
格式说明里有个极易被忽略的细节:SA、SB、CB 在低层每个仰角扫两次。以 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,但 0.5 和 1.5 各重复一次,合并后Elvation[]只有 9 个有效元素。如果按 PPI 数去开数组、按仰角序号直接索引,第 3 层之后全部错位,拼出来的垂直剖面是斜的。
判断“是不是同一层”不能只看仰角数值相等,浮点比较有风险。可靠做法是用径向头里的“径向数据状态”字段:0 是该仰角第一条,2 是该仰角最后一条,3 是体扫开始第一条,4 是体扫结束最后一条。遇到状态 0 或 3 就新开一层,遇到 2 或 4 就封层。这样即使两次扫描的仰角编码差一个最低位,也不会误判成两层。
def group_cuts(radials, band): cuts = [] cur = None for buf in radials: h = read_radial_header(buf, band) status = struct.unpack_from('<H', buf, 40)[0] # 41-42 if status in (0, 3): if cur is not None: cuts.append(cur) cur = {'el': h['el_deg'], 'rads': []} if cur is not None: cur['rads'].append((h, buf)) if status in (2, 4): cuts.append(cur) cur = None if cur is not None: cuts.append(cur) return cutsstatus在表里是 41-42,代码偏移 40。这段逻辑把“开层/封层”交给状态字段,仰角只作为记录值,不做判层依据。封层时把cur置空,防止状态 2 之后又跟一条状态 1 的脏数据被塞进已封的层。
3.2 三维数组的维度顺序与波段切换
程序里给的数组是VolRef[MaxCuts][MaxRads][RGates],MaxCuts=20,MaxRads按每度一个径向取 360。这个顺序是“层-方位-距离”,和很多绘图库默认的“方位-距离-层”相反,转格式时要做一次轴变换,否则画出来的 PPI 是横躺的。MaxRads=360意味着原始径向要按方位角重采样到整度格点,重采样时同一度内可能落进多条径向,取最近方位或做平均都行,但要在文档里写死,不然复现结果对不上。
波段切换靠DataFormat.h里的三个常量:
// S 波段 const int RGates = 460; const int VGates = 920; const int WGates = 920; // C 波段改为 // const int RGates = 800; // const int VGates = 1600; // const int WGates = 1600;改常量重新编译是最省事的做法,但工程上更稳的是运行时按文件判断:先读第一条径向的长度,2432 走 S 分支,4132 走 C 分支,再分派到对应解码函数。混批处理时,编译期常量方案会直接崩,运行时判断只多一次分支。我一般会在入口处加一道断言,把径向长度和波段标签绑死,长度对不上立刻报错,而不是让它静默读出垃圾。
3.3 指针字段的三种用法别混
头里有三组指针:65-66 反射率、67-68 速度、69-70 谱宽,这是正常数据指针;83-84、85-86、87-88 是“用于回放”的指针,语义相同但指向回放数据区。日常解码用前一组,只有在做历史回放或对比时才碰后一组。把回放指针当正常指针用,读出来的数据区会整体偏移,现象是反射率图看着像被平移了一段距离。另外,指针是“偏离雷达数据信息头的字节数”,也就是相对单条径向起点,不是相对文件起点,这一点在跨径向循环时最容易搞混。
4. 避坑排查:五个让解码结果“看着对其实错”的坑
4.1 方位角算出来是 0.0x 度
现象:整层方位角都在 0 到 1 度之间,PPI 挤成一条线。原因:双字节序读反,小端当大端。解决:struct用<H,C 里确认平台字节序,跨平台传输的文件先做一次字节序探测——取一条径向读方位角原始值,若小于 100 基本可判定读反了,因为正常方位原始值在几千量级。
4.2 速度场整体差一倍
现象:速度色标两端对称但数值只有实际的一半或两倍。原因:vel_res字段没读或读错,0.5 和 1.0 两档公式混用。解决:解码前先打印vel_res,只接受 2 和 4,其他值直接报错;两档公式分别封装,不要写成一个带if的大表达式,便于单测。
4.3 低层仰角重复导致剖面错位
现象:垂直剖面在 1.5 度附近出现两层几乎相同的数据,上层整体偏移。原因:按 PPI 计数而非按状态字段分层。解决:用 41-42 的状态字段开层封层,合并后校验len(cuts)是否等于头里“体扫内的仰角数”(45-46),不等就打印实际仰角序列人工核对。
4.4 晴空区糊上假回波
现象:无降水区域出现 -33 dBZ 左右的弱回波带。原因:反射率原始值 0 被直接代入公式。解决:解码前先判 0 和 1,分别映射到 -999.0 和 999.0,再做任何统计或绘图。这个坑在拼图时最明显,单站 PPI 反而不容易看出来。
4.5 混批文件读到一半数组越界
现象:处理一批文件,前几个正常,某个文件之后全乱或崩溃。原因:批次里混了 S 和 C 波段,径向长度常量没切。解决:入口按文件首条径向长度分派,S 走 2432/460/920,C 走 4132/800/1600,并在读每条径向时断言长度一致,不一致立即定位到具体文件和径向序号。
5. 验证解码对不对:三个交叉校验与一个习惯
解码写完,别急着出图,先做三层校验,能挡掉九成的静默错误。
第一层是字段自洽。每条径向解出的方位角应在 0 到 360 度、仰角在 0 到 20 度量级、不模糊距离在合理公里数;体扫内仰角数应等于分层后的层数;vel_res只能是 2 或 4。把这些写成断言,跑一遍全量文件,报错位置直接指向坏径向。
第二层是数值分布。反射率有效值应集中在 -30 到 70 dBZ,速度绝对值不超过 Nyquist 速度(89-90 字段,值/100 得米每秒),谱宽非负。拿一批文件统计各通道的 min/max/均值,和同型号雷达的历史统计比,量级差一截就是公式或字节序错了。
第三层是空间连续性。沿径向看,相邻库的反射率跳变不应超过十几 dBZ;沿方位看,相邻径向同一库的差异也应平滑。出现整条径向突变,多半是某条径向的指针读错或状态字段误判。
| 校验项 | 期望 | 异常指向 |
|---|---|---|
| 方位角范围 | 0–360 度 | 字节序或编码公式 |
| 速度绝对值 | ≤ Nyquist | 分辨率档位 |
| 反射率有效值 | -30–70 dBZ | 0/1 特殊值处理 |
| 分层数 | 等于头内仰角数 | 状态字段分层逻辑 |
| 径向长度 | 2432 或 4132 | 波段常量切换 |
最后说个习惯。我早期图快,直接拿现成库读,结果一批混波段数据里 C 波段文件被当 S 波段解,图能出来、色标也像样,直到做拼图才发现边缘对不上,回头重跑花了两天。从那以后,我每次接新数据源,都强制先跑一遍“读第一条径向、打印全部头字段、断言长度与波段匹配”的最小脚本,再进批量流程。这个脚本不到三十行,却能让你在十分钟内知道这份数据能不能直接用。希望帮到你。
本文还有配套的精品资源,点击获取