GVF观测数据格式说明

· 预计阅读 21 分钟

GVF 是一种面向天体测量和大地测量 VLBI 的观测数据格式。在 pSolve 中,它主要以二进制形式保存数据,文件扩展名为 .bgv。其中不仅包含群时延、时延率等观测量,还可以保存测站和射电源信息、校准数据、理论计算结果,以及解算设置和处理状态。参照Leonid写的说明文档,并结合目前的程序代码,我在这里记录下 GVF 数据库的文件组织方式。

一、GVF 格式

一次观测的 GVF 数据库通常由多个文件组成。各文件分别保存一部分数据,再由一个 .env 文件列出所使用的数据文件及其版本。例如,一个数据文件可以命名为:

20230930_j_c23273_fr1_v001.bgv

其中,20230930 是日期,j 是数据库后缀,c23273 是观测编号,fr1 表示数据分段,v001 表示该文件的版本。

按照 Leonid 的数据库命名习惯,常规 24 小时观测常用 a、b、c 等后缀,UT1 加强观测常用 i、j、k 等后缀。后缀 p 表示 Leonid 使用 PIMA 重新进行相关后处理、完成条纹拟合后生成的版本,与 IVS/CDDIS 发布的采用 FOURFIT 进行条纹拟合的版本相对应。因此,后缀也反映了数据的处理来源。

常见的数据分段包括:

分段 主要内容
fr1 基本观测信息、群时延、时延率、信噪比等
fr2 条纹拟合和通道相关的补充信息,也可能包含系统温度、相位校准等数据
cl1 气象、电缆时延等校准信息
sl1 解算设置、观测剔除标志及其他处理状态
th1 理论计算相关信息

具体哪些数据放在哪个分段,由生成程序决定。这些名称可以帮助理解文件的大致用途,详细内容还需要查看其中的数据字段。这种组织方式允许各部分分别保存和更新。例如,修改观测剔除标志后,可以更新相应的解算数据分段,而不必重写所有观测数据。不同分段也可以具有不同的版本号,由 .env 文件记录它们之间的组合关系。

二进制文件适合程序读取,但不便于直接查看。为了理解数据库中究竟保存了什么,我主要从它可以导出的文本格式 VDA 入手。

二、VDA 格式

VDA 是 VGOSDA 的简称,是用于保存和交换天体测量与大地测量 VLBI 数据的文本格式。它以群时延、时延率等 L2 观测数据为核心,同时保存描述一次观测所需的辅助信息,以及数据分析过程中产生的设置和状态。

在 pSolve 中,VDA 可以与二进制 GVF 进行转换。一份 VDA 文件可以汇集一次观测的多个数据分段,便于传输,也可以直接用文本编辑器打开。因此,对我来说,VDA 既是一种数据交换格式,也是理解数据内容的入口。这是我喜欢VDA格式的原因。

2.1 文件结构

在 pSolve 中,可以使用 gvf_transform -to_ascii 将二进制 GVF 数据库导出为 VDA。程序内部由 GVH_WRITE_AGV (源代码位于gvh/gvh/gvh_write_agv.f) 写出文本文件,当前使用的格式标识为:

VGOSDA Format of 2019.09.09

一份 VDA 文件可以包含一个或多个数据块。每个数据块包含以下内容:

标识 内容
FILE.n 该数据块对应的原始文件名
PREA.n 生成程序、创建时间等前导信息
TEXT.n 处理历史、相关报告等附加文本
TOCS.n 数据字段的名称、类别、类型、维度和说明
DATA.n 数据值;对于变长数组,还包含相应的维度信息
HEAP.n 变长数组的数据;没有这类数据时为空
CHUN.n 数据块的记录数信息

其中,n 是文件中存储数据块的编号。从二进制 GVF 导出时,每个数据块通常对应一个 .bgv 文件。例如,软件自带的观测数据库 psolve/data/20230930_j.vda.bz2 ,内部包含五个数据块:

数据块编号 对应分段
1 fr1
2 fr2
3 cl1
4 sl1
5 th1

其中的两条文件记录为:

FILE.1 /l2/gvf/int/db/20230930_j_c23273_fr1_v001.bgv
FILE.3 /l2/gvf/int/db/20230930_j_c23273_cl1_v001.bgv

第一数据块来自 fr1 文件,第三数据块来自 cl1 文件。因此,TOCS.3 和 DATA.3 都属于第三数据块,其中可以保存多个测站在许多观测段中的校准数据。编号与分段的对应关系,由当前文件中的 FILE.n 记录给出。

为了理解数据块与观测层次之间的关系,可以将几个概念放在一起:

概念 在 VDA 中的对应关系
session 一次完整观测;一份 VDA 通常保存这次观测的数据库
数据块 n 数据库中的一个存储分段,其来源由 FILE.n 给出
scan 一个观测段;相关数据通过 SCA 类字段和观测段索引保存
observation 一条基线观测记录;相关数据通过 BAS 类字段和记录序号保存
observable 群时延、相位时延率等观测量,保存在相应的 LCODE 字段中

当前写出程序会在每个数据块开始时写入格式标识,因此同一文件中可以多次出现 VGOSDA Format of 2019.09.09。

查看文件时,我主要关注 TOCS 和 DATA:前者定义字段,后者保存字段的具体值。

2.2 LCODE与数据类别

理解 VDA 的数据组织,最重要的概念是 LCODE。可以把它理解为一个有名称、有类型、有维度、有适用范围的数据字段标识。例如,GR_DELAY 保存群时延,SITNAMES 保存测站名称表。程序内部用长度为 8 个字符的名称保存这些标识,并通过名称访问相应的数据。每个字段的适用范围由其类别确定:

类别 数据对应的范围 字段示例
SES 整次观测 SITNAMES:测站名称表;SOU_COOR:射电源坐标
SCA 一个观测段 MJD_OBS、UTC_OBS:观测段的时间信息
STA 某测站在一个观测段中的数据 ATM_PRES:气压;AIR_TEMP:气温
BAS 一条基线观测记录 GR_DELAY:群时延;GRDELERR:群时延的形式误差;SNRATIO:信噪比

这种分类可以减少重复。例如,同一个观测段中,某站的气压只需保存一次,涉及该站的多条基线观测都可以使用这个值。程序通过基线观测、观测段和测站之间的索引关系找到对应数据。字段类别取决于数据的组织方式。例如,SITNAMES 属于 SES 类,因为整张测站名称表由这次观测共同使用;ATM_PRES 属于 STA 类,因为各测站的气压随观测段变化,需要分别保存。

一个 LCODE 可以保存许多组数据,每组数据又可以包含多个元素。例如,GR_DELAY 保存所有基线观测的群时延;如果有两个波段,每条基线观测就对应两个群时延。因此,定位一个具体数值需要两层信息:先确定它属于哪条基线观测记录,再确定它是该记录中哪个波段的群时延。

VDA 用四个索引表达这种关系。对于普通的定长数值字段,可以在逻辑上将其写成:

A(i1, i2, i3, i4)

其中:

  • i1、i2 定位一组数据内部的元素;
  • i3、i4 定位这组数据对应的观测记录、观测段或测站。

TOCS 给出每组数据的前两维大小,记录形式为:

TOCS.n LCODE 类别 类型 维度1 维度2 说明

DATA 则按照先定位观测对象、再定位数组元素的顺序书写:

DATA.n LCODE i3 i4 i1 i2 数值

其中,i3 和 i4 的含义由字段类别决定:

类别 i3 i4
SES 不使用,写 0 不使用,写 0
SCA 观测段序号 不使用,写 0
STA 该测站参与观测段的站内序号 测站序号
BAS 基线观测记录序号 不使用,写 0

SES 字段由整次观测共同使用,所以这两个位置都写 0。SCA 和 BAS 字段分别用一个序号确定观测段或基线观测记录。STA 字段则同时使用测站序号和站内观测段序号。这里保留两个对象索引,是为了容纳测站类数据所需要的两个编号:测站序号和该站参与的观测段序号。各类数据都保留这两个位置,不使用的位置以 0 占位,从而采用相同的记录形式。

后面的两个元素索引用于定位这一组数据内部的数值。标量的维度为 (1,1);双频群时延可以定义为 (2,1);每个通道的复相关数据则可以定义为 (2,通道数),第一维分别保存实部和虚部。

这样,四个索引就分别确定了数据所属的观测对象,以及数值在该对象内部数组中的位置。

2.3 观测段、测站与基线观测之间的关系

当前读取程序使用六个基础字段建立数据之间的对应关系:

字段 含义
NUMB_OBS 基线观测记录总数
NUMB_SCA 观测段总数
NUMB_STA 测站总数
SITNAMES 测站名称表
NOBS_STA 各测站在数据库中参与的观测段数
OBS_TAB 每条基线观测对应的观测段和两端测站

其中,OBS_TAB 是一个大小为 (3, NUMB_OBS) 的数组。对于第 k 条基线观测记录:

OBS_TAB(1,k):观测段序号
OBS_TAB(2,k):第一测站在测站名称表中的序号
OBS_TAB(3,k):第二测站在测站名称表中的序号

不同数据都是通过以上的索引建立关联。

2.4 实际样例

下面结合原始样例文件 20180102_r1823.agv说明这些信息是如何串联在一起的。

2.4.1 群时延数据

以数据文件中的第 3 条基线观测记录为例,用一段双频数据说明字段定义与数据值之间的关系:

TOCS.1 GR_DELAY   BAS  R8   2   1  Group delays per band (sec)
DATA.1 GR_DELAY 3 0  1  1 -5.320770260597998D-03

第一行定义了 GR_DELAY 字段:

内容 含义
TOCS.1 第一数据块中的字段定义
GR_DELAY 字段名
BAS 数据按基线观测记录保存
R8 8 字节双精度实数
2 1 每条基线观测对应一个大小为 (2, 1) 的数组
Group delays per band (sec) 字段说明,指出这里保存各波段的群时延,单位为秒

在这个双频样例中,数组的两个元素分别对应两个波段:

  • (1, 1) 保存第一波段的群时延;
  • (2, 1) 保存第二波段的群时延。

第二行中的 DATA 记录可以拆解为:

内容 含义
DATA.1 第一数据块中的数据
GR_DELAY 群时延字段
第一个整数 3 第 3 条基线观测记录,记为 k=3
第二个整数 0 BAS 类不使用这个索引位置,本文件以 0 填写
后两个整数 1 1 数组元素索引 (1,1),这里对应第一波段
最后的数值 群时延,单位为 s

因此,这一行保存的是第3条基线观测在第一波段的群时延: \(\tau_{\mathrm{g},1} =-5.320770260597998\times10^{-3}\ \mathrm{s} \approx-5.320770261\ \mathrm{ms}.\) 其中,D-04 是 Fortran 双精度数常用的指数写法。同一条基线观测的第二波段群时延保存在:

DATA.1 GR_DELAY 3 0  2  1 -5.320770244317517D-03

两行的基线观测序号都是 3,变化的是波段对应的元素索引。这里的 k=3 是基线观测记录的序号。同一对测站在不同观测段形成的观测,会对应不同的记录序号。如果要表示第 25 条基线观测在第二波段的群时延,索引部分就写成:

DATA.1 GR_DELAY 25 0  2  1 ...

波段的具体含义可以结合频率字段确定。在这个样例中,第一、第二波段的参考频率分别为 8.22099 GHz 和 2.22299 GHz,对应 X 波段和 S 波段。由此可见,TOCS 中的两个数字给出每组数据的数组大小,DATA 中的四个数字共同确定一个具体数值的位置。

2.4.2 从基线观测找到观测段

群时延按基线观测保存,而观测时间按观测段保存。要查找这条群时延对应的历元,需要先通过 OBS_TAB 找到它所属的观测段。OBS_TAB 的定义为:

TOCS.1 OBS_TAB    SES  I4   3 3396  Observation tables: scan index, indices of the first and the second station

它是一个大小为 (3,3396) 的整数数组,每一列对应一条基线观测:

数组元素 含义
OBS_TAB(1,k) 该条基线观测所属的观测段序号
OBS_TAB(2,k) 第一测站序号
OBS_TAB(3,k) 第二测站序号

对于第 k=3 条基线观测,文件中的记录为:

DATA.1 OBS_TAB  0 0  1  3 1
DATA.1 OBS_TAB  0 0  2  3 1
DATA.1 OBS_TAB  0 0  3  3 7

这三行共同给出:

OBS_TAB(:,3) = (1, 1, 7)

因此,第 3 条基线观测属于第 1 个观测段,记为 s=1。两端测站的序号分别为 1 和 7,稍后再用它们查询基线。

接下来,读取第 1 个观测段的日期、日内秒数和名称:

TOCS.1 MJD_OBS    SCA  I4   1   1  MJD of fringe reference time at pseudo-UTC timecale for the scan (days)
TOCS.1 UTC_OBS    SCA  R8   1   1  Pseudo-UTC time tag of fringe reference time for the scan (sec)
TOCS.1 SCANNAME   SCA  C1  16   1  Scan name

DATA.1 MJD_OBS  1 0  1  1 58120
DATA.1 UTC_OBS  1 0  1  1 6.124400000000000D+04
DATA.1 SCANNAME 1 0  1  1 002-1700a

这三个字段属于 SCA 类,按观测段保存,字段名之后的第一个整数表示观测段序号。MJD_OBS 和 UTC_OBS 各保存一个数值,SCANNAME 保存一个长度为 16 个字符的观测段名称。它们给出的信息是:

  • MJD_OBS = 58120,对应日期 2018 年 1 月 2 日;
  • UTC_OBS = 61244 s,对应日内时刻 17:00:44;
  • SCANNAME = 002-1700a,是这个观测段的名称。 其中: \(61244=17\times3600+0\times60+44.\) 所以,按文件中的 pseudo-UTC 时间标记,这条观测的条纹参考时刻为 2018 年 1 月 2 日 17:00:44。这里给出的是条纹参考时刻,不能直接当作观测段的开始时刻。

本例中,基线观测序号是 3,观测段序号是 1。因此,不能直接用 GR_DELAY 中的序号 3 去查询 MJD_OBS 和 UTC_OBS,必须先经过 OBS_TAB 的对应关系。

2.4.3 从观测段找到射电源名称和坐标

确定观测段序号 s=1 后,通过 SOU_IND 查找这个观测段对应的射电源:

TOCS.1 SOU_IND    SCA  I4   1   1  Source name index
DATA.1 SOU_IND  1 0  1  1 2

SOU_IND 属于 SCA 类。这条记录表示:第 1 个观测段观测的是源名称表中的第 2 个射电源,记为 q=2。随后读取 SRCNAMES 的第 2 项:

TOCS.1 SRCNAMES   SES  C1   8  68  Source names
DATA.1 SRCNAMES 0 0  1  2 0017+200

由此得到射电源名称 0017+200。如果需要源坐标,可以继续读取 SOU_COOR 的第 2 列:

TOCS.1 SOU_COOR   SES  R8   2  68  Source coordinates in a baricenteric reference system: right asc. decl. (rad)
DATA.1 SOU_COOR 0 0  1  2 8.565599653890889D-02
DATA.1 SOU_COOR 0 0  2  2 3.553957939493710D-01

第一行给出赤经,第二行给出赤纬,单位均为 rad: \(\alpha=0.08565599653890889\ \mathrm{rad}, \qquad \delta=0.3553957939493710\ \mathrm{rad}.\) 这里,SRCNAMES 和 SOU_COOR 都属于 SES 类,是整次观测共用的表。SOU_IND 将各个观测段与这些表中的具体条目联系起来。

2.4.4 从基线到两端测站

测站类字段同时使用测站序号和站内观测段序号。站内观测段序号按照该站在数据库中参与的观测段依次排列。前面已经得到:

OBS_TAB(:,3) = (1, 1, 7)

其中,第二个元素表示第一测站的序号为 1,第三个元素表示第二测站的序号为 7。查找测站名称表中的对应条目:

TOCS.1 SITNAMES   SES  C1   8   8  IVS site names
DATA.1 SITNAMES 0 0  1  1 FORTLEZA
DATA.1 SITNAMES 0 0  1  7 WETTZ13N

因此,第 3 条基线观测对应 FORTLEZA–WETTZ13N 基线,其中第一站是 FORTLEZA,第二站是 WETTZ13N。样例中的 STA_IND 也保存了相同的两端测站序号,可以用于核对:

TOCS.1 STA_IND    BAS  I4   2   1  Station names indexes
DATA.1 STA_IND  3 0  1  1 1
DATA.1 STA_IND  3 0  2  1 7

STA_IND 属于 BAS 类,所以这里字段名之后的第一个整数仍然是基线观测序号 3。后面的元素索引分别选择第一站和第二站,最后的数值则是测站名称表中的序号。需要保留数据库给出的两站顺序,后续解释时延符号、构造基线向量或计算理论时延时,都要与相应程序的约定保持一致。

确定测站序号 1 和 7 后,还可以查询各站的坐标。测站坐标保存在 SIT_COOR 中:

TOCS.1 SIT_COOR   SES  R8   3   8  Site coordinates in a crust-fixed terrestrial reference system: X, Y, Z (meters)

数组大小为 (3,8),每一列对应一个测站,三个元素依次为地固参考系中的 $X$、$Y$、$Z$ 坐标,单位为 m。两端测站对应的数据为:

DATA.1 SIT_COOR 0 0  1  1 4.985370006000000D+06
DATA.1 SIT_COOR 0 0  2  1 -3.955020388000000D+06
DATA.1 SIT_COOR 0 0  3  1 -4.284720880000000D+05

DATA.1 SIT_COOR 0 0  1  7 4.075627620000000D+06
DATA.1 SIT_COOR 0 0  2  7 9.317743110000000D+05
DATA.1 SIT_COOR 0 0  3  7 4.801552379000000D+06

前三行是 FORTLEZA 的坐标,后三行是 WETTZ13N 的坐标。这些是数据库保存的测站坐标;仅凭这个字段,不能确定其具体参考框架实现和坐标历元。

气温、气压等随观测变化的数据,则保存在 STA 类字段中。本例对应:

TOCS.3 AIR_TEMP   STA  R8   1   1  Air temperature at the station (K)
TOCS.3 ATM_PRES   STA  R8   1   1  Atmospheric pressure at the station (Pa)

DATA.3 AIR_TEMP 1 1  1  1 3.035866668051416D+02
DATA.3 AIR_TEMP 1 7  1  1 2.751600000000000D+02

DATA.3 ATM_PRES 1 1  1  1 1.006700000000000D+05
DATA.3 ATM_PRES 1 7  1  1 9.428000000000000D+04

两个字段均属于 STA 类,以 8 字节实数保存,每个测站在其参与的每个观测段中各对应一个数值。AIR_TEMP 的单位为 K,ATM_PRES 的单位为 Pa。以大气压字段的第二条为例,这条记录可以逐项解释为:

内容 含义
DATA.3 第三数据块中的数据记录
ATM_PRES 气压字段
前面的 1 该站参与的第 1 个观测段
接着的 7 测站序号,对应 WETTZ13N
后面的 1 1 标量的两个元素索引
9.428000000000000D+04 气压值,约为 94280 Pa

与这条基线相关的完整气象数据信息为:

测站 气温 / K 气压 / Pa
FORTLEZA 303.587 100670
WETTZ13N 275.160 94280

气象数据虽然与群时延位于不同的数据块,仍然通过观测和测站索引建立联系。

此外,STA 类的前两个索引需要单独注意,它们分别对应于测站序号和站内观测段序号。例如:

DATA.3 AIR_TEMP 1 7  1  1 2.751600000000000D+02

字段名之后的第一个整数 1,表示该测站自身参与的第 1 个观测段;第二个整数 7,表示测站名称表中的第 7 个站,即 WETTZ13N。如果某个测站缺席了前面的观测段,本站序号还可能与全局观测段序号不同。因此,读取 STA 类数据时,需要同时确定全局测站序号和该站自身的观测段序号。

如前所述,通过这些索引,一条基线观测记录可以关联到相应观测段的时间和目标源,也可以关联到两端测站的气象、电缆校准等数据。

三、读取和转换程序

3.1 VDA数据读取

实际解析 VDA 文件的函数是 GVH_READ_AGV,对应的源代码为gvh/gvh/gvh_read_agv.f。它先检查格式标识,读取字段定义和基础索引,再将其余数据放入内存中的数据结构。后续程序通过 GVH_GLCODE 按字段名称读取需要的数据。根据GVH_READ_AGV的读取逻辑,程序通过 OBS_TAB 找出该站参与的观测段,再按本站的顺序编号。前文的例子中,两站自身的第 1 个观测段都是全局第 1 个观测段,所以气象记录的首索引均为 1,不能使用基线观测序号 3。

从用户执行的命令到内部读写子程序,对应关系如下:

用途 用户执行的命令 内部负责读写的子程序
将 VDA 导入为二进制 GVF gvf_import.py,内部调用 gvf_transform -to_binary GVH_READ_AGV 读取 VDA,GVH_WRITE_BGV 写出二进制文件
将二进制 GVF 导出为 VDA gvf_transform -to_ascii GVH_READ_BGV 读取二进制文件,GVH_WRITE_AGV 写出 VDA

在已经配置好数据库目录的 pSolve 环境中,可以使用以下命令导入 VDA:

gvf_import.py /data/vda/20230930_j.vda.bz2 INT

其中,INT 是数据库目录配置中的存储库名称。(这个在哪里定义?) gvf_import.py 负责必要的解压和参数处理,再调用 gvf_transform -to_binary 写出二进制 GVF 文件及相应的 .env 文件。

反过来,可以将二进制数据库导出为 VDA:

gvf_transform -to_ascii 20230930_j_v002 /tmp/20230930_j.vda INT

上面命令中的 INT 是在 vcat.conf 中定义的存储库名称。pSolve 提供的配置模板psolve/share/vcat_conf.tmpl 包含 OBS 和 INT 两个存储库,通常分别用于常规观测和 UT1 加强观测。以 /data/gvf 为数据根目录,相关配置项可以写成:

GVF_REP_NAMES:  OBS  INT

GVF_ENV_DIR:    OBS  /data/gvf/obs/env
GVF_DB_DIR:     OBS  /data/gvf/obs/db

GVF_ENV_DIR:    INT  /data/gvf/int/env
GVF_DB_DIR:     INT  /data/gvf/int/db

这只是配置文件中与存储库有关的部分。其中:

  • GVF_REP_NAMES 登记可用的存储库名称;
  • GVF_ENV_DIR 指定 .env 文件的存放目录;
  • GVF_DB_DIR 指定二进制 .bgv 文件的存放目录。

命令末尾的 INT 明确选择了这个存储库,其用途和存放位置由配置文件决定。配置文件的路径由环境变量 VCAT_CONF 给出,查看其定义可以运行:

printenv VCAT_CONF

也可以通过下面的命令查询 pSolve 的 save 目录,再查看其中的 vcat.conf:

solve_inq --save

具体的查找过程与构建方式有关:独立构建的 gvf_transform 在未设置 VCAT_CONF 时,会先检查 PSOLVE_SAVE_DIR,再通过 solve_inq --save 查询;与 pSolve 一同构建的版本使用编译时指定的 SOLVE_SAVE_DIR。

3.2 从 vgosDB 转换为 VDA

vgosDB 数据可以通过 nuSolve 提供的 vgosDxConvertor 转换为 VDA。它是 nuSolve 软件包自带的脚本,db2da 模式表示从 vgosDB 转为 VDA:

vgosDxConvertor db2da input_vgosDB_file output_VDA_file

输入可以是 vgosDB 的数据库描述文件、包含数据库的目录,也可以是 .tgz 或 .tar.gz 归档文件。反向转换使用 da2db 模式。

例如,假设输入归档保存在以下路径,可以先转换为 VDA,再导入 pSolve:

vgosDxConvertor db2da /data/vgosdb/20230930_j.tgz /data/vda/20230930_j.vda
gvf_import.py /data/vda/20230930_j.vda INT

vgosDxConvertor 内部调用 nuSolve,并执行配套的 vgosDx.js。在 db2da 模式下,脚本先调用 importSession() 读取 vgosDB,再调用 saveDataAsVgosDa() 写出 VDA。SGDASS 中的 gvh/scripts/vdb_to_vda.py 也调用了 vgosDxConvertor db2da。实际读取 vgosDB 和写出 VDA 的工作由 nuSolve 完成。

对于 nuSolve 生成的 VDA,gvf_transform 在导入时会调用 GVH_FROM_NUSOLVE(源代码见/gvh/gvh_from_nusolve.f) 进行适配,包括调整部分字段、重新组织数据分段,并舍弃一些 nuSolve 专有字段。这些处理在转换程序说明中有所记录。通过这条转换流程,可以将 vgosDB 中的观测数据导入 pSolve,并按 pSolve 的数据组织方式保存为 GVF 数据库。