https://github.com/xieguigang/Darwinism

NetCDF文件格式(Network Common Data Format)是一种以network byteorder进行编码的CDF数据文件格式。其广泛应用于大气科学、水文、海洋学、环境模拟、地球物理等诸多数据科学计算分析领域内的数据存储。

NetCDF数据格式是由美国大学大气研究协会(University Corporation for Atmospheric Research,UCAR)的Unidata项目科学家针对科学数据的特点开发的,是一种面向数组型并适于网络共享的数据的描述和编码标准。
https://www.unidata.ucar.edu/software/netcdf/

NetCDF的文件结构

在一个NetCDF文件之中,和绝大部分的二进制文件一样,分为文件头和数据块区域。在NetCDF文件的文件头之中主要是记录了数据块之中的变量的向量长度信息,变量列表,以及整个数据集的一些全局的meta元数据等数据量较少的信息。对于在NetCDF文件中实际存储的原始数据,则是以向量的形式保存在文件头后面的数据块区域。

因为我平时在研发工作中,会经常接触到一些大的科学数据集。例如代谢组学之中的安捷伦的气质结果数据就是使用NetCDF文件保存的。因为为了工作方便,所以我自己编写了一个NetCDF的文件读取模块,开源在sciBASIC#科学计算框架开源项目之中。

如果大家想要开发自己的NetCDF数据文件引擎的话,可以参考我的sciBASIC#项目里面的CDF文件源代码进行开发: https://github.com/xieguigang/sciBASIC/tree/master/Data/BinaryData/DataStorage/netCDF

在读取NetCDF文件之中的数据,最重要的是将变量的起始位置和长度信息正确的读取出来。我定义了一下如下所示的数据变量对象用来描述对应的数据块在NetCDF文件之中的位置,源代码文件大家可以阅读variable.vb

Public Class variable

    <XmlAttribute> 
    Public Property name As String

    ''' <summary>
    ''' Array with the dimension IDs of the variable.
    ''' (<see cref="Header.dimensions"/>)
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' Dimension ID (index into dim_list) for variable
    ''' shape. We say this Is a "record variable" if And only
    ''' if the first dimension Is the record dimension.
    ''' </remarks>
    <XmlAttribute> 
    Public Property dimensions As Integer()

    Public ReadOnly Property dimensionality As dimensionality
        Get
            If dimensions.IsNullOrEmpty Then
                Return dimensionality.scalar
            ElseIf dimensions.Length = 1 Then
                Return dimensionality.vector
            Else
                Return dimensionality.matrix
            End If
        End Get
    End Property

    Public Property attributes As attribute()

    <XmlAttribute>
    Public Property type As CDFDataTypes

    ''' <summary>
    ''' Number with the size of the variable.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' Variable size. If not a record variable, the amount
    ''' of space in bytes allocated to the variable's data.
    ''' </remarks>
    <XmlAttribute> Public Property size As Integer
    ''' <summary>
    ''' Number with the offset where of the variable begins
    ''' </summary>
    ''' <returns></returns>
    <XmlAttribute> Public Property offset As UInteger
    ''' <summary>
    ''' True if Is a record variable, false otherwise
    ''' </summary>
    ''' <returns></returns>
    <XmlAttribute> Public Property record As Boolean

    Public Property value As ICDFDataVector

    Public Function GetRegion() As BufferRegion
        Return New BufferRegion With {
            .position = offset,
            .size = size
        }
    End Function
End Class

可以看到,一个在NetCDF文件之中存储的变量数据对象,其主要是由自己的变量符号名称,数据块的起始位置,数据块的长度,数据类型,dimension信息以及自己的一些attribute元数据集构成。

我在这篇文章中的重点并不是具体的详细讲解CDF的文件格式,大家如果想要详细的学习CDF文件格式的话,可以阅读这个README文档:https://github.com/xieguigang/sciBASIC/blob/master/Data/BinaryData/DataStorage/netCDF/README.md

VisualBasic程序代码读取NetCDF

在VisualBasic程序中读取NetCDF文件是通过netCDFReader对象来完成的。我们主要是通过这个对象的构造函数传递进入CDF文件的文件路径,之后通过getDataVariable函数就可以读取出目标变量的数据值了。

''' <summary>
''' Retrieves the data for a given variable
''' </summary>
''' <returns>List with the variable values</returns>
Public Function getDataVariable(variable As variable) As ICDFDataVector
    Dim values As Array

    ' go to the offset position
    Call buffer.Seek(variable.offset, SeekOrigin.Begin)

    If (variable.record) Then
        ' record variable case
        values = DataReader.record(buffer, variable, header.recordDimension)
    Else
        ' non-record variable case
        values = DataReader.nonRecord(buffer, variable)
    End If

    Return VectorHelper.FromAny(values, variable.type)
End Function

''' <summary>
''' Retrieves the data for a given variable
''' </summary>
''' <param name="variableName">Name of the variable to search Or variable object</param>
''' <returns>List with the variable values</returns>
Public Function getDataVariable(variableName As String) As ICDFDataVector
    ' search the variable
    Dim variable As variable = variableTable.TryGetValue(variableName)
    ' throws if variable Not found
    Utils.notNetcdf(variable Is Nothing, $"variable Not found: {variableName}")

    Return getDataVariable(variable)
End Function

在上面的getDataVariable数据读取函数,我提供了两个版本的读取操作:一个是从变量的文件头信息对象进行数据读取,另一个就是直接通过变量名一步到位读取出来。

通过R#脚本查看NetCDF文件内容

在通过R#脚本查看CDF文件之前,我们首先通过下面的一段VisualBasic程序代码生成一个demo用的CDF文件:

Imports Microsoft.VisualBasic.ComponentModel.Ranges.Model
Imports Microsoft.VisualBasic.Data.IO.netCDF
Imports Microsoft.VisualBasic.Data.IO.netCDF.Components
Imports randf = Microsoft.VisualBasic.Math.RandomExtensions

Dim range1 As DoubleRange = {-99, 99}

Using cdf As New CDFWriter("./dataframe.netcdf")
    ' create random data vectors as demo data
    Dim a As integers = {2, 2, 3, 4, 5, 1, 1, 1, 1, 1}
    Dim b As doubles = a.Select(Function(any) randf.GetRandomValue(range1)).ToArray
    Dim flags As flags = a.Select(Function(any) randf.NextBoolean).ToArray
    Dim id As integers = a.Select(Function(any, i) i).ToArray

    Dim data_size As New Dimension With {
        .name = "nrow",
        .size = a.Length
    }

    Call cdf.GlobalAttributes(New attribute("time", Now.ToString, CDFDataTypes.CHAR)) _
            .GlobalAttributes(New attribute("num_of_variables", 4, CDFDataTypes.INT)) _
            .GlobalAttributes(New attribute("github", "https://github.com/xieguigang/sciBASIC", CDFDataTypes.CHAR))

    Call cdf.AddVariable("a", a, data_size, {New attribute("note", "this is an integer vector", CDFDataTypes.CHAR)})
    Call cdf.AddVariable("b", b, data_size)
    Call cdf.AddVariable("flags", flags, data_size)
    Call cdf.AddVariable("id", id, data_size, {New attribute("note", "this is a unique id vector in asc order", CDFDataTypes.CHAR)})
End Using

产生的CDF文件我们可以使用记事本软件打开查看,可以看到因为是二进制文件,所以记事本显示的是乱码。但是我们在写CDF文件的时候的一些名称,注释之类的字符串还是可以很清楚的阅读出来的:

面对这个看起来一团乱的二进制数据文件,大家先不要头晕。我们可以通过下面介绍的R#脚本进行CDF文件之中的数据读取操作。在R#脚本环境之中,系统通过sciBASIC#框架自带了一个NetCDF文件的操作函数,这些操作函数都在netCDF.utils程序包模块之中。例如,我们通过下面的DEMO脚本来查看NetCDF文件里面的诸如:文件元数据信息,变量列表信息,获取变量的原始数据等操作:

require(netCDF.utils);

using dataset as open.netCDF(`${dirname(@script)}/dataframe.netcdf`) {
    print("view of the meta data of this netcdf data file:");
    print(dataset |> globalAttributes);
    print(dataset |> dimensions);

    cat("\n\n");

    const varList = dataset |> variables;
    const table   = data.frame();

    print("view of the variable list that store in this cdf file:");
    print(varList);

    for(name in varList[, "name"]) {
        table[, name] = dataset
            |> var(name)
            |> getValue
            ;
    }

    print("view of the dataframe result:");
    print(table);
}

运行一下上面的脚本,可以看到R#脚本已经很清楚地为我们打印出来了我们所需要的一些CDF文件的信息:

编写NetCDF文件的LINQ数据源驱动程序

好了,我们通过前面的介绍,已经了解到了如何通过VB代码进行cdf文件的读取操作。现在我们基于前面学习到的CDF文件的操作函数,来建立起一个用于NetCDF文件的LINQ查询数据源驱动程序吧。对于LINQ查询脚本的数据源的开发,我们可以重新回顾一下之前的文章中的学习内容:

我们和之前的Sqlite数据库驱动程序的定义一样:只需要创建一个数据源驱动程序对象,然后实现里面的一个数据源加载方法就可以了,整个工作是不是非常的简单?

Imports LINQ.Runtime.Drivers
Imports Microsoft.VisualBasic.ComponentModel.DataSourceModel
Imports Microsoft.VisualBasic.Data.IO.netCDF
Imports Microsoft.VisualBasic.Language
Imports Microsoft.VisualBasic.My.JavaScript

<DriverFlag("dataframe")>
Public Class CDFDataSet : Inherits DataSourceDriver

    Public Sub New(arguments() As String)
        MyBase.New(arguments)
    End Sub

    Public Overrides Iterator Function ReadFromUri(uri As String) As IEnumerable(Of Object)
        Using cdf As New netCDFReader(uri)
            Dim vectors As New List(Of NamedValue(Of Func(Of Integer, Object)))
            Dim size As Integer

            For Each name As String In arguments
                vectors += New NamedValue(Of Func(Of Integer, Object)) With {
                    .Name = name,
                    .Value = vectorReader(cdf, name, size),
                    .Description = size
                }
            Next

            Dim maxSize As Integer = vectors _
                .Select(Function(v)
                            Return Integer.Parse(v.Description)
                        End Function) _
                .Max

            For i As Integer = 0 To maxSize - 1
                Dim obj As New JavaScriptObject

                For Each field In vectors
                    obj(field.Name) = field.Value(i)
                Next

                Yield obj
            Next
        End Using
    End Function

    Private Shared Function vectorReader(cdf As netCDFReader, name As String, ByRef size As Integer) As Func(Of Integer, Object)
        Dim vec As Array = cdf.getDataVariable(name).genericValue

        size = vec.Length

        If size = 1 Then
            Dim [single] As Object = vec.GetValue(Scan0)

            Return Function(i) [single]
        Else
            Return AddressOf vec.GetValue
        End If
    End Function
End Class

就像前面所讲解的,我们在这个数据源驱动程序中,首先通过文件路径构建出了一个NetCDF文件的读取模块。之后呢,就可以通过变量名称,使用函数cdf.getDataVariable读取里面的向量值出来了。从上面的代码我们可以看得出来,我们的数据源驱动程序在LINQ查询引擎之中注册的名称为dataframe。所以我们只需要通过下面的类型申明,就可以读取CDF文件之中特定的数据集作为我们的查询数据来源了:

FROM x as dataframe("a", "b", "flags", "id")

例如,在上面的例子之中,我们将DEMO的CDF文件之中的a,b,flags,id这四个变量拿了出来,构成一个二维表格用于LINQ查询操作。

LINQ脚本查询NetCDF文件的DEMO

根据上面的数据源驱动程序使用介绍,我们可以编写出下面的一段LINQ查询脚本用于进行NetCDF文件的查询操作。例如,我们需要从目标CDF文件之中查找出所有的b变量大于50的向量位置所对应的所有值,之后再按照数据表投影结果之中的a ^ b的幂指数计算结果进行降序排序,可以写出如下查询脚本:

Imports "NetCDF"

FROM x as dataframe("a", "b", "flags", "id")
IN "./dataframe.netcdf"
WHERE x.b > 50
SELECT x.id, x.a, x.b, x.flags, pow = x.a ^ x.b
ORDER BY pow DESCENDING

通过LINQ命令执行一下上面写好的脚本,输出的结果和我们所设想的一样:

Latest posts by xie guigang (see all)

Attachments

No responses yet

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注