从netCDF中更快地读取时间序列?

15
我有一些大型的netCDF文件,其中包含地球上0.5度分辨率的每6小时数据。每年有360个纬度点、720个经度点和1420个时间点。我有年度文件(每个12GB)和一个包含110年数据(1.3TB)的文件,存储为netCDF-4格式(这里是1901数据的示例,1901.nc,其使用政策,以及我最初使用的原始的公共文件)。据我所知,从一个netCDF文件中读取应该比循环遍历按年份和变量分开提供的文件集合更快。

我希望能够提取每个网格点的时间序列,例如从特定纬度和经度提取10或30年的数据。然而,我发现这样做非常缓慢。例如,虽然我可以在0.002秒内读取全球10000个值的单个时间点的切片(维度顺序为纬度、经度、时间),但我需要0.01秒才能从一个点位置读取10个时间点上的值。

## a time series of 10 points from one location:
library(ncdf4)
met.nc <- nc_open("1901.nc")
system.time(a <- ncvar_get(met.nc, "lwdown", start = c(100,100,1), 
                                             count = c(1,1,10)))
   user  system elapsed 
  0.001   0.000   0.090 

## close down session

## a global slice of 10k points from one time
library(ncdf4)
system.time(met.nc <- nc_open("1901.nc"))
system.time(a <- ncvar_get(met.nc, "lwdown", start = c(100,100,1), 
                                             count = c(100,100,1)))
   user  system elapsed 
  0.002   0.000   0.002 

我猜测这些文件是为了优化空间层的阅读而编写的,因为a) 变量的顺序是纬度、经度、时间,b) 这是生成这些文件的气候模型的逻辑顺序,c) 因为全球范围是最常见的可视化方式。
我尝试重新排列变量,使时间排在第一位:
ncpdq -a time,lon,lat 1901.nc 1901_time.nc

(ncpdq来自NCO (netCDF operators)软件)

> library(ncdf4)

## first with the original data set:
> system.time(met.nc <- nc_open("test/1901.nc"))
   user  system elapsed 
  0.024   0.045  22.334 
> system.time(a <- ncvar_get(met.nc, "lwdown", start = c(100,100,1), count = c(1, 1, 1000))
+ )
   user  system elapsed 
  0.005   0.027  14.958 

## now with the rearranged dimensions:
> system.time(met_time.nc <- nc_open("test/1901_time.nc"))
   user  system elapsed 
  0.025   0.041  16.704 
> system.time(a <- ncvar_get(met_time.nc, "lwdown", start = c(100,100,1), count = c(1, 1, 1000)))
   user  system elapsed 
  0.001   0.019   9.660 

我该如何优化在某一时间点阅读时间序列,而不是一次性读取大面积的图层?例如,如果文件按时间、纬度、经度不同方式编写,那么读取速度是否更快?有没有“简单”的方法来转换netCDF-4文件中维度的顺序?
更新:
(由@mdsumner请求的基准测试)
library(rbenchmark)
library(ncdf4)
nc <- nc_open("1901.nc")
benchmark(timeseries = ncvar_get(nc, "lwdown", 
                                 start = c(1, 1, 50), 
                                 count = c(10, 10, 100)), 
          spacechunk = ncvar_get(nc, "lwdown", 
                                  start = c(1, 1, 50), 
                                  count = c(100, 100, 1)),           
          replications = 1000)

        test replications elapsed relative user.self sys.self user.child
2 spacechunk         1000   0.909    1.000     0.843    0.066          0
1 timeseries         1000   2.211    2.432     1.103    1.105          0
  sys.child
2         0
1         0

更新 2:

我已经开始在这里开发一个解决方案。这些代码碎片都在 github.com/ebimodeling/model-drivers/tree/master/met/cruncep 的一组脚本中。

这些脚本仍需要一些工作和组织 - 并非所有脚本都有用。但读取速度非常快。虽然与上述结果不完全相同,但到了一天结束时,我可以立即读取一个1.3TB文件(分辨率为0.5度,每6小时记录一次,时间跨度达100年):

system.time(ts <- ncvar_get(met.nc, "lwdown", start = c(50, 1, 1), count = c(160000, 1, 1)))
   user  system elapsed 
  0.004   0.000   0.004 

(注:维度顺序已更改,如下所述:当使用ncdf4 :: ncvar_get时,我该如何指定维度顺序?

count 表示从开头算起的单元格数量,所以第一次读取时难道不应该是 count = c(1, 1, 10) 吗? - mdsumner
@mdsumner 感谢您指出这一点。我已经更新了我的问题(使用新的、更快的时间)。 - David LeBauer
3个回答

11

我认为解决这个问题的答案不是重新排列数据,而是将数据分块。关于分块netCDF文件的影响的完整讨论,请参阅Unidata主导netCDF开发的Russ Rew的以下博客文章:

总之,虽然采用不同的分块策略可以大幅提高访问速度,但选择正确的策略并不容易。

在较小的样本数据集sst.wkmean.1990-present.nc上,当使用您的基准命令时,我看到了以下结果:

1) 未分块:

## test replications elapsed relative user.self sys.self user.child sys.child
## 2 spacechunk         1000   0.841    1.000     0.812    0.029          0         0
## 1 timeseries         1000   1.325    1.576     0.944    0.381          0         0

2) 粗略分块:

## test replications elapsed relative user.self sys.self user.child sys.child
## 2 spacechunk         1000   0.788    1.000     0.788    0.000          0         0
## 1 timeseries         1000   0.814    1.033     0.814    0.001          0         0

天真的分块方法只是试探性地尝试了一下;我用nccopy工具进行如下处理:

$ nccopy -c"lat/100,lon/100,time/100,nbnds/" sst.wkmean.1990-present.nc chunked.nc

nccopy工具的Unidata文档可以在这里找到。

我希望我能推荐一种特定的数据集分块策略,但这高度依赖于数据。希望上面链接的文章能够让您了解如何分块您的数据以实现您所需的结果!

更新

Marcos Hermida的以下博客文章展示了不同的块划分策略如何影响读取特定netCDF文件的时间序列速度。这只应被视为一个起点。

关于通过nccopy重新分块的问题,似乎与默认的块缓存大小为4MB有关。通过将其增加到4GB(甚至更多),您可以将大文件的复制时间从超过24小时减少到不到11分钟!

有一点我不确定;在第一个链接中,讨论涉及块缓存,但传递给nccopy的参数-m指定了复制缓冲区中的字节数。nccopy的-m参数控制块缓存的大小。


我的错误,我刚刚注意到较小的文件sst.wkmean.1990-present.nc是由另一个回答建议的,而不是原始帖子的作者。尽管如此,它仍然说明了分块可以产生的差异。 - Ward F.
我已经通过所有的链接了。听起来我很有可能从重新排列文件以支持时间序列中受益。但是我认为这就是nvpdq -a time,lat,lon所要做的,但我没有得到我期望的加速效果。nccopy -c"time/1420"会做到这一点吗?并且使用-u删除时间的无限属性也会使它更快吗?谢谢! - David LeBauer
我一直在尝试运行 nccopy -c 'time/1500,lat/10,lon/10' 1901.nc 1901ts.nc,但它似乎卡住了 - 大约一个小时左右它创建了一个大小相似的输出文件,但是在过去的两个小时里这个文件的大小没有变化。有什么建议吗? - David LeBauer
@David 我不熟悉nvpdq,虽然使用nccopy将“时间”维度变为最快并没有使我加速。 - Ward F.
@David,昨天我也观察到在尝试重新分块时(在1901.nc上)写入时间非常长。我认为我已经找到了一些更多的信息,并将在我的答案中更新一些链接/资源。 - Ward F.
1
哇哦。这里有一个更新:由于某种原因,即使指定了“-m 6GB”,速度也有点慢。但是改为指定“-w”并仅指定时间维度块“-c time/1460”,现在每个年度文件只需要2分钟(加上学习和实验所花费的时间)。感谢您的帮助和指引。 - David LeBauer

2

不确定您是否考虑过使用CDO提取点?

cdo remapnn,lon=x/lat=y in.nc point.nc 

有时候CDO会出现内存不够的情况,如果出现这种情况,您可能需要循环遍历年度文件,然后使用“cat”命令将各个点文件合并。
cdo mergetime point_${yyyy}.nc point_series.nc 

问题并不在于提取点本身,而是如何从单个多年文件中高效地进行提取。解决方案最终是重新组织文件以适应这种用例。 - David LeBauer

2

编辑:原问题有误,但开始读取的开销可能也不同,因此进行多次重复是公平的。使用rbenchmark可以轻松实现。

示例文件有点庞大,因此我使用了一个较小的文件,您能否使用您的文件进行相同比较?

更易访问的示例文件: ftp://ftp.cdc.noaa.gov/Datasets/noaa.oisst.v2/sst.wkmean.1990-present.nc

我得到的时间序列时间大约是两倍:

library(ncdf4)

nc <- nc_open("sst.wkmean.1990-present.nc")

library(rbenchmark)
benchmark(timeseries = ncvar_get(nc, "sst", start = c(1, 1, 50), count = c(10, 10, 100)), 
spacechunk = ncvar_get(nc, "sst", start = c(1, 1, 50), count = c(100, 100, 1)),           
replications = 1000)
##        test replications elapsed relative user.self sys.self user.child sys.child
##2 spacechunk         1000    0.47    1.000      0.43     0.03         NA        NA
##1 timeseries         1000    1.04    2.213      0.58     0.47         NA        NA

感谢您的建议;我已经更新了我的问题,并附上了基准测试结果。 - David LeBauer
我意识到,在第一次之后,速度会更快,因为信息仍然存储在内存中。所以,如果我重复使用ncvar_get命令,第一次可能需要10秒钟,而下一次只需要0.001秒。但是感谢您指出这种计时方法。 - David LeBauer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接