Fortran Sequential二进制文件读入问题诊断
问题
现有一个二进制文件rain_clmn_202306.0000.dat及对应的GrADS CTL数据描述文件,使用GrADS绘图正常显示,但通过Fortran程序读入数据会报错。
CTL数据描述文件如下所示:
DSET ^rain_clmn_202306.0000.dat
OPTIONS sequential big_endian
UNDEF 9999.000
TITLE Result for CTRL,MWR,
XDEF 45 linear 5.0000 9.0000
YDEF 8 levels 3.0000 6.0000 9.0000 12.0000 15.0000 18.0000 21.0000 24.0000
ZDEF 1 levels 0.000
TDEF 30 linear 00Z01JUN2023 1dy
VARS 12
CTRL1 1 0 CTRL (TS)
CTRL2 1 0 CTRL (MISSED)
CTRL3 1 0 CTRL (FALSEA)
CTRL4 1 0 CTRL (EFFICENT)
CTRL5 1 0 CTRL (BIAS)
CTRL6 1 0 CTRL (ETS)
MWR1 1 0 MWR (TS)
MWR2 1 0 MWR (MISSED)
MWR3 1 0 MWR (FALSEA)
MWR4 1 0 MWR (EFFICENT)
MWR5 1 0 MWR (BIAS)
MWR6 1 0 MWR (ETS)
ENDVARS
通过CTL文件可知二进制数据是Fortran sequential unformatted格式数据,其端序是大端(big_endian)。数据布局是有30个时次数据,每个时次12个变量,其中每个变量只有单层(45*8
)数据。
通过下面Fortran代码读入
integer :: ivar,itime
real*4,dimension(45,8) :: buf
open(99,file="rain_clmn_202306.0000.dat",form="unformatted",access="sequential",convert="big_endian")
do itime=1,30
do ivar=1,12
print*, 'ivar=',ivar,'itime=',itime
read(99) buf
enddo
enddo
执行报错:
ivar= 1 itime= 1
ivar= 2 itime= 1
forrtl: severe (24): end-of-file during read, unit 99, file rain_clmn_202306.0000.dat
以上Fortran程序完全按照CTL描述文件读取,为何在第二个记录读取时报错?
此外,使用CDO工具将该二进制文件转换成netcdf格式也能成功转换。
$ cdo -b f32 -f nc import_binary rain_clmn_202306.0000.ctl rain_00.nc
$ ncdump -h rain_00.nc
那么问题出在哪里?
Sequential unformatted数据介绍
Sequential无格式数据是Fortran标准支持的一种独有的数据格式。对于传统的基于记录的Fortran数据格式,sequential访问的无格式数据在文件中会存储记录长度。
每个记录体(Body)前后有记录头和记录尾,分别占有4个字节,存储了本记录的长度信息。读取流程大致是:Fortran运行时会先读取记录头中的记录长度信息,然后读取相应的记录数据,完成后文件指针转到下一条记录头,重复以上过程。
前面提到的CTL描述文件,每个记录长度应该是45x8x4(bytes)=1440
,因此每条记录的记录头/尾应该存储整数值1440。
诊断
前面Fortran程序读取错误,我们可以将第一个记录头信息打印出来看看记录长度是多少。
integer*4 :: recl
! 这里我们使用stream格式访问
open(99,file="rain_clmn_202306.0000.dat",form="unformatted",access="stream",convert="big_endian")
read(99) recl
print*,recl
打印出来的结果是538976288,不是我们预期的值。文件大小为521280字节,如果程序根据这个记录长度读取,在读第二个记录时已经超出文件大小了,因此程序报错end-of-file
也就不足为奇了。
通过以上分析,可以判断这个sequential二进制数据有问题的。
我们可以写一个简单的Fortran转换程序生成正确的sequential二进制数据。
思路是通过流(stream)格式访问读入获取记录体数据,然后写出到sequential数据。
integer*4 :: recl
integer :: ivar,itime
real*4,dimension(45,8) :: buf
! 以stream方式访问原数据 open(99,file="rain_clmn_202306.0000.dat",form="unformatted",access="stream",convert="big_endian")
! 生成sequential访问格式数据 open(100,file="new.dat",form="unformatted",access="sequential",convert="big_endian")
do itime=1,30
do ivar=1,12
read(99) recl ! 读入记录头,不用
read(99) buf
read(99) recl ! 读入记录尾,不用
write(100) buf ! 写出记录体数据到sequential数据
enddo
enddo
我们可以验证一下新生成的数据记录头/尾记录数据是多少。
$ od --endian=big -N4 -t d4 new.dat
0000000 1440
0000004
可以看出记录头中存储了正确的记录长度信息1440。
现在还剩一个问题未解决,为何GrADS或者CDO能正确处理这个二进制文件呢?
我们来查看CDO源代码来看看是如何处理的。
CDO是由C/C++实现的,import_binary功能是在Importbinary.cc
代码中实现的。
处理核心代码如下:
读取每条记录数据
size_t rc = fread(rec.data(), 1, recsize, pfi.infile);
这里的recsize长度对应前面数据应该是recsize=pfi.gsiz *4
。
gridsize是水平x/y维格点总数,对应以上数据是45*8
。
如果为sequential访问格式,记录偏移量recoffset要加4个字节,真正记录体数据从第5个字节开始提取。
char *cdata = &rec[recoffset]
因此cdo在转换二进制文件时是完全根据CTL描述文件,通过流格式读入(fread)整条记录,再截取记录体数据。不像Fortran是根据读入的记录头中记录长度信息来进一步读取记录体,也就不会遇到问题。GrADS处理方式与cdo相同。
小结
- Fortran sequential二进制数据是基于记录格式数据,每条记录包含记录头/记录体/记录尾。
- Fortran读取sequential二进制数据与工具CDO/GrADS(C/C++实现方式)方式不同。