carray-netcdf 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -2
- data/CONTRIBUTING.md +3 -3
- data/README.md +53 -21
- data/bin/ncheader +43 -8
- data/bin/ncvar +48 -12
- data/carray-netcdf.gemspec +4 -4
- data/docs/00_introduction.md +19 -30
- data/docs/01_getting_started.md +16 -25
- data/docs/02_reading.md +26 -26
- data/docs/03_writing.md +53 -44
- data/docs/04_codec.md +74 -82
- data/docs/05_groups.md +5 -5
- data/docs/06_low_level_api.md +161 -0
- data/docs/07_cookbook.md +167 -126
- data/docs/08_cheatsheet.md +77 -52
- data/docs/09_cli.md +4 -4
- data/lib/carray/netcdf/codec.rb +223 -29
- data/lib/carray/netcdf/dim.rb +8 -5
- data/lib/carray/netcdf/file.rb +184 -57
- data/lib/carray/netcdf/group.rb +301 -43
- data/lib/carray/netcdf/var.rb +311 -31
- data/lib/carray/netcdf/version.rb +1 -1
- data/lib/carray/netcdf.rb +99 -17
- metadata +7 -7
- data/docs/06_layer1.md +0 -160
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b62bd15777ba74f1034591a396904bdce84015051c9895c9f01e1793ea39685
|
|
4
|
+
data.tar.gz: fc6d94c6f079e92081035f999d3a12d111622202e858b64c7f06fb61e06e0d9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa102781a9705ea42e3a9b96ff95e7d34c4f2dc2204512d468f16866085bde6f35f2115f41d9e90a4ca4d25d424708380c7c8efeed092e7eb2f43d92c68d1210
|
|
7
|
+
data.tar.gz: f5841947884c2e91e72c334cc3884576778f36b71d8bf0d1aa5a537591893b5f26982a06fe5529c5e9eac7206af427131853bf973ba9a5b276cc33f851fa5fb0
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,77 @@ different codebase and is not covered.
|
|
|
30
30
|
netcdf gem's. Which of those a release needs is said in the gemspec,
|
|
31
31
|
and an entry says so only when the answer changes. -->
|
|
32
32
|
|
|
33
|
+
## 2.1.0
|
|
34
|
+
|
|
35
|
+
- New: `def_var` and `File.create` take `zstandard:`, `bzip2:`, `szip:`
|
|
36
|
+
and `blosc:` beside `deflate:`. zstandard, bzip2 and blosc need
|
|
37
|
+
libnetcdf 4.9; a filter libnetcdf lacks raises `NetCDF::Error` rather
|
|
38
|
+
than writing the data uncompressed.
|
|
39
|
+
|
|
40
|
+
- New: `Var#storage_options` gives a variable's chunking, compression and
|
|
41
|
+
other storage settings in `def_var`'s keyword form, and
|
|
42
|
+
`File#netcdf4?` says whether the file can have them.
|
|
43
|
+
|
|
44
|
+
- New: `File.create(format:)` makes `:classic`, `:offset64` (CDF-2),
|
|
45
|
+
`:cdf5`, `:netcdf4` or `:netcdf4_classic` files. A template's format
|
|
46
|
+
is kept, and `netcdf4: false` wins over a NetCDF-4 template.
|
|
47
|
+
|
|
48
|
+
- New: `File#reload` / `Group#reload` read the file's dimensions,
|
|
49
|
+
variables, groups and attributes again, after changes made through the
|
|
50
|
+
low-level API.
|
|
51
|
+
|
|
52
|
+
- New: `Var#att_type` gives the `NC_*` type an attribute is stored in.
|
|
53
|
+
|
|
54
|
+
- New: a file with CF attributes but no `Conventions` (as xarray and
|
|
55
|
+
netCDF4-python write) warns once that it is read as stored, and
|
|
56
|
+
suggests `codec: :cf`.
|
|
57
|
+
|
|
58
|
+
- Change: these are private now: `CA::NetCDF.nc_type_for`,
|
|
59
|
+
`.put_att_typed` and `.put_var_att`; the low-level get / put calls on
|
|
60
|
+
`File`, `Group`, `Var` and `Dim`; `Group#copy_att_from` and
|
|
61
|
+
`File.atomic_target`. Pass a type name to `def_var`, call
|
|
62
|
+
`NetCDF.nc_put_att_*`, or call the get / put functions on `CA::NetCDF`.
|
|
63
|
+
|
|
64
|
+
- Change: attribute values from `atts` / `att`, and what `dims` / `vars`
|
|
65
|
+
/ `groups` return, are frozen. Code that changes them in place should
|
|
66
|
+
copy them first.
|
|
67
|
+
|
|
68
|
+
- Change: `File.create(..., clobber: false)` on an existing file raises
|
|
69
|
+
`Errno::EEXIST`, also without `atomic:`; it raised `NetCDF::Error`.
|
|
70
|
+
Code that rescued that should rescue `Errno::EEXIST`.
|
|
71
|
+
|
|
72
|
+
- Change: needs Ruby 3.2 or later, as CArray 3.0 does, and the netcdf gem
|
|
73
|
+
0.2.0 or later (below 0.3).
|
|
74
|
+
|
|
75
|
+
- Change: `File#abort`, and the block form when its block raises, undo
|
|
76
|
+
the definitions made since the file last left define mode; they were
|
|
77
|
+
kept. An aborted non-atomic `File.create` removes the file.
|
|
78
|
+
|
|
79
|
+
- Change: `def_var(template:)`, `copy_var` and `File.create(vars:)` copy
|
|
80
|
+
the template's storage settings when both files are NetCDF-4.
|
|
81
|
+
Compression options you pass replace the template's as a whole. Chunk
|
|
82
|
+
sizes are copied only when the dimensions have the same lengths.
|
|
83
|
+
|
|
84
|
+
- Change: float32 `scale_factor` / `add_offset` unpack in float32, as CF
|
|
85
|
+
asks and xarray does; they unpacked in float64.
|
|
86
|
+
|
|
87
|
+
- Change: a CArray given as `_FillValue`, `missing_value` or `valid_*` is
|
|
88
|
+
written in the variable's type, as a Ruby value is. A masked one
|
|
89
|
+
raises `ArgumentError`.
|
|
90
|
+
|
|
91
|
+
- Change: reading by an index array on an axis, and asking a variable's
|
|
92
|
+
size, `first` or `last`, no longer read the whole variable.
|
|
93
|
+
|
|
94
|
+
- Fix: a float written to an integer variable is rounded, and NaN
|
|
95
|
+
written to an integer or packed variable becomes the fill value (or
|
|
96
|
+
raises when there is none). 2.0.0 truncated the float and stored NaN as
|
|
97
|
+
`0`, so check integer data that 2.0.0 wrote from floats.
|
|
98
|
+
|
|
99
|
+
- Fix: under the standard calendar, a CF time reference before
|
|
100
|
+
1582-10-15 is a Julian date, as cftime reads it, and a one- to
|
|
101
|
+
three-digit reference year is that year. 2.0.0 read such times wrong
|
|
102
|
+
(`"days since 1-1-1"` as 2001).
|
|
103
|
+
|
|
33
104
|
## 2.0.0
|
|
34
105
|
|
|
35
106
|
A ground-up rewrite. Nothing from 1.x is carried over, so read this as a
|
|
@@ -49,12 +120,12 @@ new library that happens to keep the name rather than as an upgrade.
|
|
|
49
120
|
binding — is included rather than modified, so a future `Numo::NetCDF`
|
|
50
121
|
can sit beside it. Code written against 1.x's names does not run here.
|
|
51
122
|
|
|
52
|
-
- New:
|
|
123
|
+
- New: the low-level API, `CA::NetCDF`'s own `nc_*` functions. They are the
|
|
53
124
|
netcdf gem's API with the array I/O overridden to take and return
|
|
54
125
|
CArrays; definition, inquiry, attribute, and group calls pass through
|
|
55
126
|
verbatim. Usable as module functions or by `include CA::NetCDF`.
|
|
56
127
|
|
|
57
|
-
- New:
|
|
128
|
+
- New: the high-level API, the object-oriented surface — `File`, `Group`, `Var`,
|
|
58
129
|
`Dim`. `File.open` / `File.create`, `nc[:temp]` to read a variable,
|
|
59
130
|
`nc[:temp, 0..9] = data` to write a hyperslab, lazy hyperslab reads,
|
|
60
131
|
file-wide variable option defaults, and atomic writes through a
|
data/CONTRIBUTING.md
CHANGED
|
@@ -46,7 +46,7 @@ And, where they apply, the things most bugs here turn on:
|
|
|
46
46
|
|
|
47
47
|
## Sending a fix
|
|
48
48
|
|
|
49
|
-
Ruby 3.
|
|
49
|
+
Ruby 3.2 or later, CArray and the netcdf gem in the ranges the gemspec declares. Sibling checkouts of `carray` and `netcdf` beside this one are picked up automatically when they are there; otherwise the installed gems are used.
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
52
|
rake spec # the whole suite (also the default task)
|
|
@@ -66,9 +66,9 @@ Keep the commit message to a subject line and a line or two of context. Anything
|
|
|
66
66
|
|
|
67
67
|
## Working on the code
|
|
68
68
|
|
|
69
|
-
`docs/` is the guide and is written to be read in order. The code is one file per class under `lib/carray/netcdf/`, with
|
|
69
|
+
`docs/` is the guide and is written to be read in order. The code is one file per class under `lib/carray/netcdf/`, with the low-level API — the `nc_*` functions that speak CArray — in `lib/carray/netcdf.rb` itself.
|
|
70
70
|
|
|
71
|
-
The line worth holding while working here:
|
|
71
|
+
The line worth holding while working here: the low-level API is raw values only. No convention decoding, no mask handling; reads come back unmasked and writing a masked CArray raises. Everything about `_FillValue`, `scale_factor`, and time belongs to the codecs in the high-level API.
|
|
72
72
|
|
|
73
73
|
## License
|
|
74
74
|
|
data/README.md
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
# carray-netcdf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read and write [NetCDF](https://www.unidata.ucar.edu/software/netcdf/) files from Ruby, with variable data read into and written from [CArray](https://github.com/himotoyoshi/carray)s.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
CArray is a Ruby library of multi-dimensional arrays, each holding a single numeric type, with slicing, element-wise arithmetic, and statistics over the whole array or along any axis. Every element can be masked as missing, and the statistics skip masked elements.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The gem is built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem, a near one-to-one binding to libnetcdf, and offers two APIs on top of it:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
* **Low-level API** — the netcdf gem's `nc_*` functions, with the ones that read and write variable data taking and returning CArray.
|
|
10
|
+
* **High-level API** — an object-oriented API (`File`, `Group`, `Var`, `Dim`) that applies the file's conventions, and is what everyday code uses.
|
|
10
11
|
|
|
11
12
|
## Status
|
|
12
13
|
|
|
13
|
-
2.0
|
|
14
|
+
The 2.0 API is not settled yet, and may change between releases.
|
|
14
15
|
|
|
15
16
|
## Features
|
|
16
17
|
|
|
17
|
-
* **Object-oriented
|
|
18
|
-
* **
|
|
19
|
-
* **
|
|
20
|
-
* **
|
|
21
|
-
* **
|
|
22
|
-
* **Atomic writes.**
|
|
18
|
+
* **Object-oriented API.** Files, groups, variables and dimensions are objects.
|
|
19
|
+
* **Partial data access.** Only the part of a variable you index is read or written, so a variable larger than memory can be handled a piece at a time.
|
|
20
|
+
* **CF and COARDS conventions.** Files whose `Conventions` attribute names CF or COARDS are read as the conventions define them: missing values as masks, packed values unpacked, and time axes as dates and times. Writing reverses each of these.
|
|
21
|
+
* **Definitions and data in any order.** Dimensions, variables and attributes can be defined and data written in whatever order suits you; NetCDF's define and data modes are switched as needed.
|
|
22
|
+
* **Existing files as templates.** A new file can take its dimensions, global attributes and coordinate variables from an existing file, and any variable can take its definition from a variable in another file.
|
|
23
|
+
* **Atomic writes.** On request, a file appears under its name only once it has been written completely, so a failed run leaves no half-written file behind.
|
|
23
24
|
|
|
24
25
|
## Install
|
|
25
26
|
|
|
@@ -45,12 +46,38 @@ gem "carray-netcdf"
|
|
|
45
46
|
|
|
46
47
|
Requires:
|
|
47
48
|
|
|
48
|
-
- Ruby >= 3.
|
|
49
|
-
-
|
|
49
|
+
- Ruby >= 3.2
|
|
50
|
+
- libnetcdf >= 4.1, with its headers (`nc-config` or `pkg-config` on PATH)
|
|
51
|
+
- A C compiler, to build the netcdf gem
|
|
52
|
+
- carray >= 3.0.1, installed as a dependency
|
|
50
53
|
- the netcdf gem, installed as a dependency
|
|
51
54
|
|
|
52
55
|
## Example
|
|
53
56
|
|
|
57
|
+
### Low-level API
|
|
58
|
+
|
|
59
|
+
The `nc_*` functions that read and write variable data are on `CA::NetCDF` and take and return CArray; the others, such as opening and looking up, are called on the netcdf gem's `NetCDF` as they are. Files and variables are integer ids, and the values come back as stored, so fill values, packing and time units are yours to handle:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require "carray-netcdf"
|
|
63
|
+
|
|
64
|
+
NC = CA::NetCDF
|
|
65
|
+
|
|
66
|
+
ncid = NetCDF.nc_open("data.nc", NetCDF::NC_NOWRITE) # the netcdf gem, as it is
|
|
67
|
+
vid = NetCDF.nc_inq_varid(ncid, "temp")
|
|
68
|
+
|
|
69
|
+
day = NC.nc_get_vara(ncid, vid, [0, 0, 0], [1, 73, 144]) # a new CArray, as stored
|
|
70
|
+
|
|
71
|
+
buf = CArray.float32(73, 144)
|
|
72
|
+
NC.nc_get_vara(ncid, vid, [0, 0, 0], [1, 73, 144], buf) # or read into one you have
|
|
73
|
+
|
|
74
|
+
NetCDF.nc_close(ncid)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### High-level API
|
|
78
|
+
|
|
79
|
+
Files and variables by name, and a variable indexed like a CArray. The CF or COARDS conventions a file declares in its `Conventions` attribute are applied automatically — here, the `_FillValue` element comes back masked:
|
|
80
|
+
|
|
54
81
|
```ruby
|
|
55
82
|
require "carray-netcdf"
|
|
56
83
|
|
|
@@ -65,9 +92,11 @@ NC::File.create("out.nc") do |nc|
|
|
|
65
92
|
nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
66
93
|
units: "K", _FillValue: -999.9)
|
|
67
94
|
|
|
68
|
-
nc.def_att("Conventions", "CF-1.7")
|
|
95
|
+
nc.def_att("Conventions", "CF-1.7") # turns CF encoding and decoding on
|
|
69
96
|
|
|
70
|
-
|
|
97
|
+
temp = CArray.float32(1, 73, 144).random(30) + 260 # random values, 260-290
|
|
98
|
+
temp[0, 0, 0] = UNDEF # mark one element missing
|
|
99
|
+
nc["temp"] = temp # written as -999.9
|
|
71
100
|
end
|
|
72
101
|
|
|
73
102
|
# --- read ---
|
|
@@ -76,23 +105,26 @@ NC::File.open("out.nc") do |nc|
|
|
|
76
105
|
t.shape # => [1, 73, 144]
|
|
77
106
|
t.dims.map(&:name) # => ["time", "lat", "lon"]
|
|
78
107
|
|
|
79
|
-
day = t[0, nil, nil] # a
|
|
80
|
-
day.
|
|
108
|
+
day = t[0, nil, nil] # a 73 x 144 CArray; only this part is read
|
|
109
|
+
day.count_masked # => 1, the -999.9 came back masked
|
|
110
|
+
day.mean # the mean of the other 10511 elements
|
|
81
111
|
end
|
|
82
112
|
```
|
|
83
113
|
|
|
84
114
|
## Documentation
|
|
85
115
|
|
|
86
|
-
* [Introduction](docs/00_introduction.md) —
|
|
116
|
+
* [Introduction](docs/00_introduction.md) — what the gem does, what it leaves to CArray, and how it sits on the netcdf gem
|
|
87
117
|
* [Getting started](docs/01_getting_started.md) — install, then read and write a first file
|
|
88
118
|
* [Reading files](docs/02_reading.md) — opening a file, finding a variable, reading all or part of it
|
|
89
119
|
* [Writing files](docs/03_writing.md) — declaring the schema, writing data, changing a file later
|
|
90
120
|
* [Codecs and CF conventions](docs/04_codec.md) — what gets decoded, how a codec is chosen, writing your own
|
|
91
121
|
* [Groups](docs/05_groups.md) — NetCDF-4 groups, and what they inherit
|
|
92
|
-
* [
|
|
122
|
+
* [Low-level API](docs/06_low_level_api.md) — the `nc_*` functions with CArray in and out
|
|
93
123
|
* [Cookbook](docs/07_cookbook.md) — worked solutions to real tasks
|
|
94
124
|
* [Cheatsheet](docs/08_cheatsheet.md) — every method and constant of the gem on one page
|
|
95
|
-
* [Command-line tools](docs/09_cli.md) — `ncheader` and `ncvar
|
|
125
|
+
* [Command-line tools](docs/09_cli.md) — `ncheader` and `ncvar`
|
|
126
|
+
|
|
127
|
+
What changed between releases is in [CHANGELOG.md](CHANGELOG.md).
|
|
96
128
|
|
|
97
129
|
## Contributing
|
|
98
130
|
|
|
@@ -106,4 +138,4 @@ carray-netcdf was designed and reviewed by a human developer; the implementation
|
|
|
106
138
|
|
|
107
139
|
## License
|
|
108
140
|
|
|
109
|
-
MIT
|
|
141
|
+
[MIT](LICENSE)
|
data/bin/ncheader
CHANGED
|
@@ -27,6 +27,15 @@ PRIMITIVE_TYPE_NAMES = {
|
|
|
27
27
|
NetCDF::NC_STRING => "string",
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
# A String as valid UTF-8, for JSON / YAML and the terminal: a byte that
|
|
31
|
+
# is not part of a UTF-8 character is written as \xNN (old files often
|
|
32
|
+
# hold Latin-1 text).
|
|
33
|
+
def printable_text(s)
|
|
34
|
+
# String.new, not dup.force_encoding: before carray 3.0.7, a string from
|
|
35
|
+
# CArray#dump_binary kept a stale "valid" flag through force_encoding.
|
|
36
|
+
String.new(s, encoding: Encoding::UTF_8).scrub { |b| b.unpack("C*").map { |c| format("\\x%02X", c) }.join }
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
def type_name(ncid, xtype)
|
|
31
40
|
PRIMITIVE_TYPE_NAMES[xtype] || NetCDF.nc_inq_type(ncid, xtype).first
|
|
32
41
|
end
|
|
@@ -35,10 +44,20 @@ def collect_attrs(ncid, varid, natts)
|
|
|
35
44
|
natts.times.map do |i|
|
|
36
45
|
name = NetCDF.nc_inq_attname(ncid, varid, i)
|
|
37
46
|
xtype, _len = NetCDF.nc_inq_att(ncid, varid, name)
|
|
47
|
+
value = begin
|
|
48
|
+
NetCDF.nc_get_att(ncid, varid, name)
|
|
49
|
+
rescue NotImplementedError
|
|
50
|
+
"(not readable: #{type_name(ncid, xtype)} attribute)"
|
|
51
|
+
end
|
|
52
|
+
value = case value
|
|
53
|
+
when String then printable_text(value).sub(/\u0000+\z/, "") # as ncdump drops it
|
|
54
|
+
when Array then value.map { |v| v.is_a?(String) ? printable_text(v) : v }
|
|
55
|
+
else value
|
|
56
|
+
end
|
|
38
57
|
{
|
|
39
|
-
"name" => name,
|
|
58
|
+
"name" => printable_text(name),
|
|
40
59
|
"type" => type_name(ncid, xtype),
|
|
41
|
-
"value" =>
|
|
60
|
+
"value" => value,
|
|
42
61
|
}
|
|
43
62
|
end
|
|
44
63
|
end
|
|
@@ -73,11 +92,13 @@ def collect_user_types(ncid)
|
|
|
73
92
|
end
|
|
74
93
|
|
|
75
94
|
def inspect_group(ncid)
|
|
76
|
-
_, _, natts,
|
|
95
|
+
_, _, natts, _ = NetCDF.nc_inq(ncid)
|
|
96
|
+
# Every unlimited dimension of the group (NetCDF-4 can have several).
|
|
97
|
+
unlimited = NetCDF.nc_inq_unlimdims(ncid)
|
|
77
98
|
|
|
78
99
|
dims = NetCDF.nc_inq_dimids(ncid, false).map { |id|
|
|
79
100
|
name, len = NetCDF.nc_inq_dim(ncid, id)
|
|
80
|
-
{ "name" => name, "length" => len, "unlimited" => (id
|
|
101
|
+
{ "name" => name, "length" => len, "unlimited" => unlimited.include?(id) }
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
vars = NetCDF.nc_inq_varids(ncid).map { |vid|
|
|
@@ -113,6 +134,18 @@ TYPE_SUFFIX = {
|
|
|
113
134
|
"float" => "f", "double" => "",
|
|
114
135
|
}
|
|
115
136
|
|
|
137
|
+
# A float32 value with the fewest digits that read back as the same
|
|
138
|
+
# float32, as ncdump prints it (`0.1`, not 0.10000000149011612).
|
|
139
|
+
def float32_text(x)
|
|
140
|
+
return x.to_s unless x.finite?
|
|
141
|
+
bits = [x].pack("f")
|
|
142
|
+
(1..9).each do |digits|
|
|
143
|
+
s = format("%.#{digits}g", x)
|
|
144
|
+
return s if [Float(s)].pack("f") == bits
|
|
145
|
+
end
|
|
146
|
+
x.to_s
|
|
147
|
+
end
|
|
148
|
+
|
|
116
149
|
def cdl_escape(s)
|
|
117
150
|
s.to_s.gsub(/[\\"\n\r\t]/, "\\" => '\\\\', '"' => '\\"',
|
|
118
151
|
"\n" => '\\n', "\r" => '\\r', "\t" => '\\t')
|
|
@@ -127,7 +160,7 @@ def cdl_value(type, value)
|
|
|
127
160
|
suffix = TYPE_SUFFIX[type] || ""
|
|
128
161
|
arr.map { |v|
|
|
129
162
|
if v.is_a?(Float)
|
|
130
|
-
type == "float" ? "#{v}f" : v.to_s
|
|
163
|
+
type == "float" ? "#{float32_text(v)}f" : v.to_s
|
|
131
164
|
else
|
|
132
165
|
"#{v}#{suffix}"
|
|
133
166
|
end
|
|
@@ -173,7 +206,8 @@ def emit_cdl(io, group, name, indent = 0)
|
|
|
173
206
|
dims = v["dimensions"].empty? ? "" : "(#{v["dimensions"].join(", ")})"
|
|
174
207
|
io.puts "#{pre} #{v["type"]} #{v["name"]}#{dims} ;"
|
|
175
208
|
v["attributes"].each { |a|
|
|
176
|
-
|
|
209
|
+
prefix = a["type"] == "string" ? "string " : ""
|
|
210
|
+
io.puts "#{pre} #{prefix}#{v["name"]}:#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
|
|
177
211
|
}
|
|
178
212
|
}
|
|
179
213
|
end
|
|
@@ -183,7 +217,8 @@ def emit_cdl(io, group, name, indent = 0)
|
|
|
183
217
|
label = indent.zero? ? "global attributes" : "group attributes"
|
|
184
218
|
io.puts "#{pre}// #{label}:"
|
|
185
219
|
group["attributes"].each { |a|
|
|
186
|
-
|
|
220
|
+
prefix = a["type"] == "string" ? "string " : ""
|
|
221
|
+
io.puts "#{pre} #{prefix}:#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
|
|
187
222
|
}
|
|
188
223
|
end
|
|
189
224
|
|
|
@@ -218,6 +253,6 @@ end
|
|
|
218
253
|
|
|
219
254
|
case format
|
|
220
255
|
when "cdl" then emit_cdl($stdout, tree, tree["name"])
|
|
221
|
-
when "json" then puts JSON.pretty_generate(tree)
|
|
256
|
+
when "json" then puts JSON.pretty_generate(tree, allow_nan: true) # NaN / Infinity as bare words
|
|
222
257
|
when "yaml" then puts YAML.dump(tree)
|
|
223
258
|
end
|
data/bin/ncvar
CHANGED
|
@@ -35,6 +35,32 @@ TYPE_NAMES = {
|
|
|
35
35
|
NetCDF::NC_STRING => "string",
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
# A String as valid UTF-8, for JSON / YAML and the terminal: a byte that
|
|
39
|
+
# is not part of a UTF-8 character is written as \xNN (old files often
|
|
40
|
+
# hold Latin-1 text).
|
|
41
|
+
def printable_text(s)
|
|
42
|
+
# String.new, not dup.force_encoding: before carray 3.0.7, a string from
|
|
43
|
+
# CArray#dump_binary kept a stale "valid" flag through force_encoding.
|
|
44
|
+
String.new(s, encoding: Encoding::UTF_8).scrub { |b| b.unpack("C*").map { |c| format("\\x%02X", c) }.join }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# printable_text over a nested Array of Strings.
|
|
48
|
+
def deep_text(a)
|
|
49
|
+
a.map { |x| x.is_a?(Array) ? deep_text(x) : printable_text(x) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A float32 value with the fewest digits that read back as the same
|
|
53
|
+
# float32, as ncdump prints it (`0.1`, not 0.10000000149011612).
|
|
54
|
+
def float32_text(x)
|
|
55
|
+
return x.to_s unless x.finite?
|
|
56
|
+
bits = [x].pack("f")
|
|
57
|
+
(1..9).each do |digits|
|
|
58
|
+
s = format("%.#{digits}g", x)
|
|
59
|
+
return s if [Float(s)].pack("f") == bits
|
|
60
|
+
end
|
|
61
|
+
x.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
38
64
|
def parse_int_list(s)
|
|
39
65
|
return nil if s.nil?
|
|
40
66
|
s.split(",").map { |x| Integer(x) }
|
|
@@ -77,13 +103,20 @@ begin
|
|
|
77
103
|
shape_full = dimids.map { |d| NetCDF.nc_inq_dimlen(gid, d) }
|
|
78
104
|
|
|
79
105
|
start = parse_int_list(start_arg) || [0] * shape_full.size
|
|
80
|
-
count = parse_int_list(count_arg) || shape_full.zip(start).map { |full, s| full - s }
|
|
81
106
|
stride = parse_int_list(stride_arg)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
abort "start/stride rank mismatch (var has #{shape_full.size} dims)" \
|
|
108
|
+
unless start.size == shape_full.size && (stride.nil? || stride.size == shape_full.size)
|
|
109
|
+
start.each_with_index do |s, i|
|
|
110
|
+
unless (0..shape_full[i]).cover?(s)
|
|
111
|
+
abort "start #{s} is out of range for dimension #{i} of length #{shape_full[i]}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
# By default, the rest of each dimension, taking every stride-th element.
|
|
115
|
+
steps = stride || [1] * shape_full.size
|
|
116
|
+
abort "stride must be positive" unless steps.all?(&:positive?)
|
|
117
|
+
count = parse_int_list(count_arg) ||
|
|
118
|
+
shape_full.each_index.map { |i| (shape_full[i] - start[i] + steps[i] - 1) / steps[i] }
|
|
119
|
+
abort "count rank mismatch (var has #{shape_full.size} dims)" unless count.size == shape_full.size
|
|
87
120
|
|
|
88
121
|
shape = count.dup
|
|
89
122
|
|
|
@@ -119,8 +152,8 @@ begin
|
|
|
119
152
|
"type" => "string",
|
|
120
153
|
"shape" => shape,
|
|
121
154
|
"start" => start,
|
|
122
|
-
"data" => data,
|
|
123
|
-
})
|
|
155
|
+
"data" => data.is_a?(Array) ? deep_text(data) : printable_text(data),
|
|
156
|
+
}, allow_nan: true)
|
|
124
157
|
end
|
|
125
158
|
else
|
|
126
159
|
# data is a CArray; its bytes are the native-endian stream.
|
|
@@ -135,15 +168,18 @@ begin
|
|
|
135
168
|
puts unless bytes.end_with?("\n")
|
|
136
169
|
else
|
|
137
170
|
abort "type #{xtype} not supported for text output" if opaque
|
|
138
|
-
data.to_a.flatten
|
|
171
|
+
values = data.to_a.flatten
|
|
172
|
+
values = values.map { |v| v.is_a?(Float) ? float32_text(v) : v } if xtype == NetCDF::NC_FLOAT
|
|
173
|
+
values.each { |v| puts v }
|
|
139
174
|
end
|
|
140
175
|
when "json"
|
|
141
176
|
payload =
|
|
142
177
|
if xtype == NetCDF::NC_CHAR
|
|
143
|
-
bytes
|
|
178
|
+
printable_text(bytes)
|
|
144
179
|
else
|
|
145
180
|
abort "type #{xtype} not supported for json output" if opaque
|
|
146
|
-
data.to_a.flatten
|
|
181
|
+
values = data.to_a.flatten
|
|
182
|
+
xtype == NetCDF::NC_FLOAT ? values.map { |v| v.is_a?(Float) && v.finite? ? Float(float32_text(v)) : v } : values
|
|
147
183
|
end
|
|
148
184
|
out = {
|
|
149
185
|
"name" => varspec,
|
|
@@ -152,7 +188,7 @@ begin
|
|
|
152
188
|
"start" => start,
|
|
153
189
|
"data" => payload,
|
|
154
190
|
}
|
|
155
|
-
puts JSON.pretty_generate(out)
|
|
191
|
+
puts JSON.pretty_generate(out, allow_nan: true) # NaN / Infinity as bare words
|
|
156
192
|
end
|
|
157
193
|
end
|
|
158
194
|
ensure
|
data/carray-netcdf.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.license = "MIT"
|
|
19
19
|
spec.homepage = "https://github.com/himotoyoshi/carray-netcdf"
|
|
20
20
|
|
|
21
|
-
spec.required_ruby_version = ">= 3.
|
|
21
|
+
spec.required_ruby_version = ">= 3.2"
|
|
22
22
|
|
|
23
23
|
spec.files = Dir[
|
|
24
24
|
"lib/**/*.rb",
|
|
@@ -34,8 +34,8 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.bindir = "bin"
|
|
35
35
|
spec.executables = %w[ncheader ncvar]
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
37
|
+
# netcdf is capped at the next minor: its 0.x minors may change calls
|
|
38
|
+
# this gem makes. 0.2.0 brings the compression filter calls.
|
|
39
39
|
spec.add_dependency "carray", ">= 3.0.1"
|
|
40
|
-
spec.add_dependency "netcdf", ">= 0.
|
|
40
|
+
spec.add_dependency "netcdf", ">= 0.2.0", "< 0.3"
|
|
41
41
|
end
|
data/docs/00_introduction.md
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
# Introduction
|
|
2
2
|
|
|
3
|
-
carray-netcdf is a NetCDF interface for CArray, built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem.
|
|
3
|
+
The carray-netcdf gem is a NetCDF interface for CArray, built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem. Using it takes some familiarity with both NetCDF and CArray. If either is new to you, Unidata's [NetCDF data model](https://docs.unidata.ucar.edu/netcdf-c/current/netcdf_data_model.html) explains dimensions, variables and attributes, the [CF conventions](https://cfconventions.org/) explain fill values, packing and time, and the [CArray](https://github.com/himotoyoshi/carray) page introduces the arrays. This guide covers what the gem adds between the two.
|
|
4
4
|
|
|
5
5
|
## What is carray-netcdf
|
|
6
6
|
|
|
7
|
-
A NetCDF file holds arrays, and
|
|
7
|
+
A NetCDF file holds arrays, and this gem hands them to you as CArrays. The purpose of this gem is to connect the two directly: open a file, get CArrays out of it, compute, and put CArrays back into a file that other NetCDF tools can read.
|
|
8
8
|
|
|
9
|
-
Two things stand between a file and a CArray you can compute on. The first is the values themselves. What a file stores is often not what you want to work with — a physical field packed into `short` with a `scale_factor`, missing values marked by a `_FillValue` sentinel, a time axis kept as numbers since an epoch. Reading those correctly means following the conventions the file declares, and the gem does it for you, so what comes back is a masked CArray of physical values and a CATime
|
|
9
|
+
Two things stand between a file and a CArray you can compute on. The first is the values themselves. What a file stores is often not what you want to work with — a physical field packed into `short` with a `scale_factor`, missing values marked by a `_FillValue` sentinel, a time axis kept as numbers since an epoch. Reading those correctly means following the conventions the file declares in its `Conventions` attribute, and when that names CF or COARDS the gem does it for you, so what comes back is a masked CArray of physical values and a time axis as CATime, CArray's array of dates and times.
|
|
10
10
|
|
|
11
11
|
The second is size. Files are routinely larger than memory, so reading a variable has to mean reading a part of it. Here a variable is indexed the way a CArray is, and only the part you index is read.
|
|
12
12
|
|
|
13
|
-
## Two
|
|
13
|
+
## Two APIs, one module
|
|
14
14
|
|
|
15
|
-
The netcdf gem is a
|
|
15
|
+
The netcdf gem is a near one-to-one binding to libnetcdf, and reads and writes variable data in memory you provide. Two APIs turn that into CArray, and both live in the module `CA::NetCDF`:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
netcdf (dependency)
|
|
18
|
+
netcdf (dependency) near one-to-one binding to libnetcdf
|
|
19
19
|
↑
|
|
20
20
|
CA::NetCDF
|
|
21
|
-
├─
|
|
22
|
-
└─
|
|
21
|
+
├─ low-level the nc_* functions, with CArray in and out
|
|
22
|
+
└─ high-level File / Group / Var / Dim, and the codecs
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The low-level API keeps the `nc_*` functions as they are and only changes what they take and return. A read given no buffer returns a new CArray of the variable's type and shape; a write takes a CArray, and refuses one with masked elements, since the low-level API applies no convention. Call the get and put functions on `CA::NetCDF` and the rest on the netcdf gem's `NetCDF`. Or, for the C idiom with CArray at both ends, mix `CA::NetCDF` into a class of your own and call all of them bare.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
The high-level API is built on the low-level one and is the everyday surface: a file is an object, a variable is indexed like a CArray, and a codec — the object that applies a convention such as CF — decodes values when reading and encodes them when writing.
|
|
28
28
|
|
|
29
29
|
## Convention in this guide
|
|
30
30
|
|
|
@@ -35,38 +35,27 @@ require "carray-netcdf"
|
|
|
35
35
|
NC = CA::NetCDF
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Requiring the gem loads CArray, the netcdf gem, and both
|
|
38
|
+
Requiring the gem loads CArray, the netcdf gem, and both APIs.
|
|
39
39
|
|
|
40
40
|
## What the gem covers
|
|
41
41
|
|
|
42
|
-
Whole NetCDF files, read and written: dimensions including unlimited ones, variables of every type the netcdf gem binds, attributes on a variable or on the file, and NetCDF-4 groups.
|
|
42
|
+
Whole NetCDF files, read and written: dimensions including unlimited ones, variables of every type the netcdf gem binds, attributes on a variable or on the file, and NetCDF-4 groups. For a file that declares CF or COARDS, values pass through a codec in both directions, so `_FillValue`, `scale_factor`, `add_offset` and CF time are handled when reading and when writing. A file can be created atomically, or with its schema copied from an existing file.
|
|
43
43
|
|
|
44
44
|
## Threads and Ractors
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
The GVL is released while variable data is read or written, so a large read or write does not hold up other Ruby threads. Do not touch a CArray from another thread while it is being read into or written from.
|
|
47
47
|
|
|
48
|
-
libnetcdf itself is not thread-safe, so the netcdf gem lets one call at a time into it, across the whole process.
|
|
48
|
+
libnetcdf itself is not thread-safe, so the netcdf gem lets one call at a time into it, across the whole process. Threads therefore do not make NetCDF I/O faster, only the Ruby work around it. That lock covers single calls: a `File`, `Group` or `Var` keeps state of its own between calls, so share one between threads only under a lock of your own.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
This gem works only from the main Ractor. Opening a file in any other Ractor raises. For NetCDF I/O in parallel, use separate processes.
|
|
51
|
+
|
|
52
|
+
The [cookbook](07_cookbook.md#11-read-in-one-thread-compute-in-another) shows a reader thread feeding a thread that computes.
|
|
51
53
|
|
|
52
54
|
## Beyond this gem
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
Two parts of CF are left to you, or to a library built on this one:
|
|
55
57
|
|
|
56
|
-
- **The CF data model.** Coordinate systems, grid mappings, cell methods, standard names and ancillary variables are kept as plain attributes — `var.att("grid_mapping")` still works — but no coordinate objects are built from them.
|
|
58
|
+
- **The CF data model.** Coordinate systems, grid mappings, cell methods, standard names and ancillary variables are kept as plain attributes — `var.att("grid_mapping")` still works — but no coordinate objects are built from them.
|
|
57
59
|
- **Non-standard calendars** for CF time (`noleap`, `360_day`, `julian` and the rest). CATime is proleptic Gregorian, in UTC, so a file declaring another calendar warns once and its time coordinate comes back as the raw numbers.
|
|
58
|
-
- **Time finer than CATime's base tick.** A variable in `"hours since 2022-01-01"` decodes to whole hours. Declare a finer unit, such as `"seconds since ..."`, if you need sub-hour precision.
|
|
59
60
|
|
|
60
61
|
The [codec chapter](04_codec.md) states in full which CF attributes are honoured and which are passed through untouched.
|
|
61
|
-
|
|
62
|
-
## How this guide is arranged
|
|
63
|
-
|
|
64
|
-
- [Getting started](01_getting_started.md) — install, then read and write a first file
|
|
65
|
-
- [Reading files](02_reading.md) — opening a file, finding a variable, reading all or part of it
|
|
66
|
-
- [Writing files](03_writing.md) — declaring the schema, writing data, changing a file later
|
|
67
|
-
- [Codecs and CF conventions](04_codec.md) — what gets decoded, how a codec is chosen, writing your own
|
|
68
|
-
- [Groups](05_groups.md) — NetCDF-4 groups, and what they inherit
|
|
69
|
-
- [Layer 1](06_layer1.md) — the `nc_*` functions with CArray in and out
|
|
70
|
-
- [Cookbook](07_cookbook.md) — worked solutions to real tasks
|
|
71
|
-
- [Cheatsheet](08_cheatsheet.md) — every method and constant of the gem on one page
|
|
72
|
-
- [Command-line tools](09_cli.md) — `ncheader` and `ncvar`, which ship with the gem
|