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
data/docs/01_getting_started.md
CHANGED
|
@@ -16,13 +16,13 @@ Then:
|
|
|
16
16
|
gem install carray-netcdf
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
That brings in CArray and the netcdf gem.
|
|
20
|
-
|
|
21
19
|
Requires:
|
|
22
20
|
|
|
23
|
-
- Ruby >= 3.
|
|
24
|
-
-
|
|
25
|
-
-
|
|
21
|
+
- Ruby >= 3.2
|
|
22
|
+
- libnetcdf >= 4.1, with its headers (`nc-config` or `pkg-config` on PATH)
|
|
23
|
+
- A C compiler, to build the netcdf gem
|
|
24
|
+
- carray >= 3.0.1, installed as a dependency
|
|
25
|
+
- the netcdf gem, installed as a dependency
|
|
26
26
|
|
|
27
27
|
## Loading
|
|
28
28
|
|
|
@@ -32,25 +32,23 @@ require "carray-netcdf"
|
|
|
32
32
|
NC = CA::NetCDF # optional short alias used throughout this guide
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Loading the gem pulls in carray, the netcdf gem, layer 1 (the `nc_*` functions), and layer 2 (`File`, `Group`, `Var`, `Dim`, `Codec`).
|
|
36
|
-
|
|
37
35
|
## Reading in three lines
|
|
38
36
|
|
|
39
37
|
```ruby
|
|
40
38
|
NC::File.open("data.nc") do |nc|
|
|
41
|
-
data = nc["
|
|
39
|
+
data = nc["temp"].to_ca # => CArray (already decoded via CF codec if applicable)
|
|
42
40
|
end
|
|
43
41
|
```
|
|
44
42
|
|
|
45
43
|
- `NC::File.open` opens for reading by default. The block form auto-closes.
|
|
46
|
-
- `nc["
|
|
47
|
-
- `.to_ca` reads the whole variable and returns a `CArray`. If the file
|
|
44
|
+
- `nc["temp"]` returns a lazy `Var` handle — no I/O yet.
|
|
45
|
+
- `.to_ca` reads the whole variable and returns a `CArray`. If the file's `Conventions` attribute names CF or COARDS and the variable has `_FillValue` / `scale_factor` / `add_offset` / CF time units, all of that is decoded automatically.
|
|
48
46
|
|
|
49
47
|
## Reading a slice
|
|
50
48
|
|
|
51
49
|
```ruby
|
|
52
50
|
NC::File.open("data.nc") do |nc|
|
|
53
|
-
t = nc["
|
|
51
|
+
t = nc["temp"]
|
|
54
52
|
t.shape # => e.g. [365, 73, 144]
|
|
55
53
|
t.dims.map(&:name) # => ["time", "lat", "lon"]
|
|
56
54
|
|
|
@@ -60,7 +58,7 @@ NC::File.open("data.nc") do |nc|
|
|
|
60
58
|
end
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
The `[]` accepts
|
|
61
|
+
The `[]` accepts any index a CArray accepts. Integers, ranges, strides and `nil` for a whole axis read only the part you index; an index array reads the block its indices span and picks from it, and a boolean mask reads the whole variable.
|
|
64
62
|
|
|
65
63
|
## Writing a file in ten lines
|
|
66
64
|
|
|
@@ -76,13 +74,13 @@ NC::File.create("out.nc") do |nc|
|
|
|
76
74
|
nc.def_att("Conventions", "CF-1.7")
|
|
77
75
|
nc.def_att("title", "Demo file")
|
|
78
76
|
|
|
79
|
-
nc["temp"] = CArray.float32(1, 73, 144)
|
|
77
|
+
nc["temp"] = CArray.float32(1, 73, 144).random(30) + 260
|
|
80
78
|
end
|
|
81
79
|
```
|
|
82
80
|
|
|
83
81
|
- `File.create` opens the file in **define mode** and drops you into it.
|
|
84
|
-
- `def_dim` / `def_var` / `def_att` build the schema. Pass `:unlimited`
|
|
85
|
-
- Variable attributes go as keyword arguments to `def_var` (any Ruby-side Numeric / String / Array is fine).
|
|
82
|
+
- `def_dim` / `def_var` / `def_att` build the schema. Pass `:unlimited` for an unlimited dimension. Types are Symbols: `:byte :short :int :int64 :float :double`, plus the CArray-style aliases (`:int32`, `:float32`, `:float64` …).
|
|
83
|
+
- Variable attributes go as keyword arguments to `def_var` (any Ruby-side Numeric / String / Array is fine). `_FillValue`, `missing_value`, `valid_range`, `valid_min` and `valid_max` are written in the variable's type, as CF expects; libnetcdf itself requires it of `_FillValue`.
|
|
86
84
|
- `nc["temp"] = ca` is a whole-variable write. `nc["temp", 0, nil, nil] = ca` is a hyperslab write. `File` switches between define and data mode for you.
|
|
87
85
|
|
|
88
86
|
## The three access idioms
|
|
@@ -99,16 +97,9 @@ NC::File.open("data.nc") do |nc|
|
|
|
99
97
|
slab = nc["temp"][0..9, nil, nil]
|
|
100
98
|
|
|
101
99
|
# (3) treat the Var as if it were a CArray
|
|
102
|
-
#
|
|
103
|
-
# but each call
|
|
100
|
+
# a method a Var doesn't have is called on its to_ca — convenient for
|
|
101
|
+
# interactive work, but each call reads the whole variable again, so
|
|
102
|
+
# keep the CArray if you loop.
|
|
104
103
|
nc["temp"].mean
|
|
105
104
|
end
|
|
106
105
|
```
|
|
107
|
-
|
|
108
|
-
## Where to go next
|
|
109
|
-
|
|
110
|
-
- [Reading files](02_reading.md) — the full `File` / `Var` / `Dim` surface, plus what codecs turn raw bytes into (masks, physical values, CATime).
|
|
111
|
-
- [Writing files](03_writing.md) — schema definition, hyperslab writes, packing, unlimited dimensions.
|
|
112
|
-
- [Codecs and CF conventions](04_codec.md) — what CF decoding actually does, how to opt in to `valid_range` masking, how to write custom codecs.
|
|
113
|
-
- [Groups](05_groups.md) — NetCDF-4 groups.
|
|
114
|
-
- [Cookbook](07_cookbook.md) — end-to-end scenarios pulled from real use cases.
|
data/docs/02_reading.md
CHANGED
|
@@ -10,8 +10,6 @@ ca = var[0, nil, nil] # this is the read: libnetcdf fills a CArray, the cod
|
|
|
10
10
|
|
|
11
11
|
A `Var` is a lazy handle. Holding one costs nothing, and the index you give it decides how much of the variable is read — a point, a slab, a strided subset, or the whole thing with `to_ca`. What comes back is a CArray, with the file's conventions already applied to it, so a packed variable arrives as physical values and its missing entries arrive as a mask.
|
|
12
12
|
|
|
13
|
-
Everything else on this page is detail around those three steps.
|
|
14
|
-
|
|
15
13
|
## Opening
|
|
16
14
|
|
|
17
15
|
```ruby
|
|
@@ -42,15 +40,15 @@ NC::File.open("data.nc", :append) # alias of :write
|
|
|
42
40
|
|
|
43
41
|
For creating a new file, see [Writing files](03_writing.md).
|
|
44
42
|
|
|
45
|
-
A Zarr store opens the same way, through a URL in place of the path: `NC::File.open("file:///data/t.zarr#mode=zarr,file")`.
|
|
43
|
+
A Zarr store opens the same way, through a URL in place of the path: `NC::File.open("file:///data/t.zarr#mode=zarr,file")`. See [Zarr stores](03_writing.md#zarr-stores).
|
|
46
44
|
|
|
47
45
|
## Introspection
|
|
48
46
|
|
|
49
47
|
```ruby
|
|
50
48
|
NC::File.open("data.nc") do |nc|
|
|
51
49
|
nc.filename # => "data.nc" (OS path)
|
|
52
|
-
nc.format # =>
|
|
53
|
-
nc.path # => "/" (
|
|
50
|
+
nc.format # => NetCDF::NC_FORMAT_CLASSIC, NetCDF::NC_FORMAT_NETCDF4, ... (Integer constants)
|
|
51
|
+
nc.path # => "/" (NetCDF-4 group path; "/" for the root)
|
|
54
52
|
|
|
55
53
|
nc.dims # => {"time" => Dim, "lat" => Dim, "lon" => Dim}
|
|
56
54
|
nc.vars # => {"temp" => Var, "time" => Var, "lat" => Var, "lon" => Var}
|
|
@@ -65,7 +63,7 @@ NC::File.open("data.nc") do |nc|
|
|
|
65
63
|
end
|
|
66
64
|
```
|
|
67
65
|
|
|
68
|
-
Names are Strings
|
|
66
|
+
Names are Strings; a Symbol is accepted too, so `nc[:temp]` is the same as `nc["temp"]`.
|
|
69
67
|
|
|
70
68
|
## The Var handle
|
|
71
69
|
|
|
@@ -94,28 +92,31 @@ t.codec # => the codec that will decode reads from this variable
|
|
|
94
92
|
ca = nc["temp"].to_ca # equivalent to nc["temp"][]
|
|
95
93
|
```
|
|
96
94
|
|
|
97
|
-
Returns a CArray of shape `t.shape` and type `t.data_type` (or the codec's choice, e.g.
|
|
95
|
+
Returns a CArray of shape `t.shape` and type `t.data_type` (or the codec's choice, e.g. `:float64` after scale/offset, CATime after CF time decode). The codec has already run — `_FillValue` cells are `UNDEF`, physical values are unpacked, time coordinates are CATime.
|
|
96
|
+
|
|
97
|
+
An `NC_STRING` variable has no CArray type, so reading it through a `Var` raises `NotImplementedError`. Read it with the netcdf gem, which returns Strings: `NetCDF.nc_get_var(nc.ncid, nc["names"].varid)`.
|
|
98
98
|
|
|
99
99
|
## Hyperslab reads (`nc[var][i, j, k]`)
|
|
100
100
|
|
|
101
|
-
`Var#[]` accepts
|
|
101
|
+
`Var#[]` accepts any index a CArray accepts:
|
|
102
102
|
|
|
103
103
|
```ruby
|
|
104
104
|
t = nc["temp"]
|
|
105
105
|
|
|
106
106
|
t[] # whole variable (same as t.to_ca)
|
|
107
107
|
t[0, nil, nil] # first time step (rank-2 CArray, shape [73, 144])
|
|
108
|
-
t[0] # first
|
|
109
|
-
#
|
|
108
|
+
t[0] # first element: a single integer is a flat address, as on a CArray;
|
|
109
|
+
# give every axis to get a slab
|
|
110
110
|
t[0, 0, 0] # single element (scalar — Float / Integer / Element)
|
|
111
111
|
t[0..9, nil, nil] # first 10 time steps
|
|
112
|
-
t[(0..-1).step(2), nil, nil] # every other time step
|
|
113
|
-
t[[0, 5, 10], nil, nil]
|
|
112
|
+
t[(0..-1).step(2), nil, nil] # every other time step
|
|
113
|
+
t[CA_INT([0, 5, 10]), nil, nil] # time steps 0, 5 and 10 (reads steps 0..10, then picks)
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
116
|
+
- A point index (`0, 0, 0`) returns a **decoded scalar** — a `Float` after scale/offset, `UNDEF` for a fill cell, a `CATime::Element` for a CF time coordinate, or a plain Numeric if the variable has no codec transforms.
|
|
117
|
+
- A range, a stride or `nil` reads only the elements it covers. An Array of three integers is `[start, count, stride]`, as on a CArray, so `t[[0, 5, 2], nil, nil]` reads time steps 0, 2, 4, 6 and 8.
|
|
118
|
+
- An index array on an axis reads the block from its smallest to its largest index, and picks from that; a few flat addresses (`t[CA_INT([1, 7, -1])]`) are read one by one. A boolean mask covers the whole variable, so it reads all of it.
|
|
119
|
+
- `elements`, `size`, `dim0`..`dim3`, `first` and `last` are answered from the file without reading the variable.
|
|
119
120
|
|
|
120
121
|
Scalar decoding examples:
|
|
121
122
|
|
|
@@ -127,14 +128,13 @@ nc["time"][0].to_time # => 2022-01-01 00:00:00 UTC (Ruby Time)
|
|
|
127
128
|
nc["time"][0].to_date # => #<Date: 2022-01-01>
|
|
128
129
|
```
|
|
129
130
|
|
|
130
|
-
##
|
|
131
|
+
## Using a Var like a CArray
|
|
131
132
|
|
|
132
|
-
`Var`
|
|
133
|
+
A method a `Var` doesn't have is called on its `to_ca`, so anything you can call on a CArray, you can call on a Var directly:
|
|
133
134
|
|
|
134
135
|
```ruby
|
|
135
136
|
nc["temp"].mean # => the mean of the whole array
|
|
136
137
|
nc["temp"].max # => the max
|
|
137
|
-
nc["temp"].shape # (Var#shape is defined, so this is not method_missing)
|
|
138
138
|
nc["temp"].sum(0) # => reduction along axis 0
|
|
139
139
|
```
|
|
140
140
|
|
|
@@ -151,22 +151,22 @@ temp.sum(0)
|
|
|
151
151
|
|
|
152
152
|
## Reading a subset into an existing buffer
|
|
153
153
|
|
|
154
|
-
For streaming big files a slab at a time, allocate one buffer once and reuse it.
|
|
154
|
+
For streaming big files a slab at a time, allocate one buffer once and reuse it. The low-level API's `nc_get_vara` accepts an explicit CArray as the last argument and reads into it — no allocation per slab:
|
|
155
155
|
|
|
156
156
|
```ruby
|
|
157
157
|
NC::File.open("big.nc") do |nc|
|
|
158
|
-
varid =
|
|
158
|
+
varid = nc["temp"].varid
|
|
159
159
|
buf = CArray.float32(73, 144) # one time slice, reused
|
|
160
160
|
|
|
161
161
|
365.times do |k|
|
|
162
162
|
CA::NetCDF.nc_get_vara(nc.ncid, varid, [k, 0, 0], [1, 73, 144], buf)
|
|
163
|
-
# buf now holds day k
|
|
163
|
+
# buf now holds day k, as stored (not decoded)
|
|
164
164
|
process(buf)
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
This
|
|
169
|
+
This skips the codec, so use it when you want the stored values and no allocation per read. See the [low-level API chapter](06_low_level_api.md) for all of it.
|
|
170
170
|
|
|
171
171
|
## Dimensions
|
|
172
172
|
|
|
@@ -189,7 +189,7 @@ Attributes are surfaced as native Ruby values by the underlying netcdf gem:
|
|
|
189
189
|
|
|
190
190
|
- `NC_CHAR` / `NC_STRING` → `String`
|
|
191
191
|
- Numeric scalar (single-element) → `Integer` or `Float`
|
|
192
|
-
- Numeric multi-element →
|
|
192
|
+
- Numeric multi-element → Ruby `Array`
|
|
193
193
|
|
|
194
194
|
Examples:
|
|
195
195
|
|
|
@@ -197,15 +197,15 @@ Examples:
|
|
|
197
197
|
nc.att("Conventions") # => "CF-1.7"
|
|
198
198
|
nc.att("title") # => "Demo file"
|
|
199
199
|
nc["temp"].att("units") # => "K"
|
|
200
|
-
nc["temp"].att("_FillValue") # => -999.
|
|
201
|
-
nc["temp"].att("valid_range") # =>
|
|
200
|
+
nc["temp"].att("_FillValue") # => -999.9000244140625 (-999.9 stored as float)
|
|
201
|
+
nc["temp"].att("valid_range") # => [0.0, 400.0]
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
An unknown attribute returns `nil`. `nc.atts` returns the frozen hash of everything (global on `nc`, per-variable on `nc["temp"]`).
|
|
205
205
|
|
|
206
206
|
## Turning decoding off
|
|
207
207
|
|
|
208
|
-
If you want
|
|
208
|
+
If you want the stored values — no `_FillValue` masking, no unpacking, no CATime — pass `codec: :identity`:
|
|
209
209
|
|
|
210
210
|
```ruby
|
|
211
211
|
NC::File.open("data.nc", codec: :identity) do |nc|
|
data/docs/03_writing.md
CHANGED
|
@@ -27,12 +27,17 @@ NC::File.create("out.nc") do |nc|
|
|
|
27
27
|
# ... define dims/vars/atts, then write data
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# NetCDF-4 (needed for groups, compression, unlimited
|
|
30
|
+
# NetCDF-4 (needed for groups, compression, chunking, and more than one unlimited dimension)
|
|
31
31
|
NC::File.create("out.nc", netcdf4: true) do |nc|
|
|
32
32
|
# ...
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
#
|
|
35
|
+
# Any format by name: :classic, :offset64 (CDF-2), :cdf5, :netcdf4, :netcdf4_classic
|
|
36
|
+
NC::File.create("big.nc", format: :cdf5) do |nc|
|
|
37
|
+
# ...
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Refuse to overwrite an existing file (raises Errno::EEXIST)
|
|
36
41
|
NC::File.create("out.nc", clobber: false)
|
|
37
42
|
|
|
38
43
|
# Atomic write: publish only on success. Bytes go to a same-directory
|
|
@@ -40,15 +45,13 @@ NC::File.create("out.nc", clobber: false)
|
|
|
40
45
|
# Any exception from the block unlinks the tmp; the target file is
|
|
41
46
|
# untouched (an earlier version, if any, survives intact).
|
|
42
47
|
NC::File.create("out.nc", atomic: true) do |nc|
|
|
43
|
-
# ... define + write. On raise: tmp cleaned up, "out.nc"
|
|
48
|
+
# ... define + write. On raise: tmp cleaned up, "out.nc" not created /
|
|
44
49
|
# overwritten. On success: "out.nc" appears atomically at end of block.
|
|
45
50
|
end
|
|
46
51
|
```
|
|
47
52
|
|
|
48
53
|
**Atomic write caveat.** Consumers polling the target path won't see anything until close — for a long-running unlimited-dim streaming write where a downstream reader wants incremental visibility, prefer `atomic: false` (the default).
|
|
49
54
|
|
|
50
|
-
`create` opens the file in **define mode**. The block is the natural place to declare the schema; `File` will drop out of define mode automatically before the first data write, and back into it for further definitions.
|
|
51
|
-
|
|
52
55
|
## Defining dimensions
|
|
53
56
|
|
|
54
57
|
```ruby
|
|
@@ -57,7 +60,7 @@ nc.def_dim("lat", 73) # fixed length
|
|
|
57
60
|
nc.def_dim("lon", 144)
|
|
58
61
|
```
|
|
59
62
|
|
|
60
|
-
`:unlimited`
|
|
63
|
+
`:unlimited` creates an unlimited dimension. Every other value is coerced with `Integer(...)`. `def_dim` returns the `Dim` and stores it under `nc.dims[name]`.
|
|
61
64
|
|
|
62
65
|
## Defining variables
|
|
63
66
|
|
|
@@ -74,7 +77,7 @@ nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
|
74
77
|
- 3rd arg: list of dimension names (Strings). An empty list makes a scalar.
|
|
75
78
|
- Keyword args: attributes to attach at define time. Any Ruby-side Numeric, String, or Array is fine.
|
|
76
79
|
|
|
77
|
-
**Type-matched attributes.** `_FillValue`, `missing_value`, `valid_range`, `valid_min`, and `valid_max` must be written in the variable's on-disk type (CF §2.5.1 and libnetcdf
|
|
80
|
+
**Type-matched attributes.** `_FillValue`, `missing_value`, `valid_range`, `valid_min`, and `valid_max` must be written in the variable's on-disk type (CF §2.5.1 asks for this, and libnetcdf requires it of `_FillValue`). The gem handles that for you, so `_FillValue: -999.9` on a `:float` variable is stored as a float. Other attributes take a NetCDF type inferred from the Ruby value.
|
|
78
81
|
|
|
79
82
|
**NetCDF-4 storage / filter options.** `def_var` recognizes these keyword arguments alongside the attributes (only effective on a NetCDF-4 file):
|
|
80
83
|
|
|
@@ -84,7 +87,11 @@ nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
|
84
87
|
| `chunks: :contiguous` | force `NC_CONTIGUOUS` |
|
|
85
88
|
| `chunks: :compact` | `NC_COMPACT` (small variables only) |
|
|
86
89
|
| `deflate: 4` | zlib compression level 1..9 (0 / omitted / `nil` = off) |
|
|
87
|
-
| `shuffle: true` | shuffle filter (usually paired with
|
|
90
|
+
| `shuffle: true` | shuffle filter (usually paired with a compressor) |
|
|
91
|
+
| `zstandard: 3` | zstandard compression level (0 / omitted / `nil` = off) |
|
|
92
|
+
| `bzip2: 9` | bzip2 compression level 1..9 (0 / omitted / `nil` = off) |
|
|
93
|
+
| `szip: { coding: :nn, pixels_per_block: 32 }` | szip compression; `coding:` is `:nn` or `:ec` |
|
|
94
|
+
| `blosc: { compressor: :lz4, level: 5, blocksize: 0, shuffle: :byte }` | blosc compression; `compressor:` is `:lz`, `:lz4`, `:lz4hc`, `:snappy`, `:zlib` or `:zstd`, and `shuffle:` is `:none`, `:byte` or `:bit` |
|
|
88
95
|
| `fletcher32: true` | per-chunk fletcher32 checksum |
|
|
89
96
|
| `endian: :little / :big / :native` | on-disk byte order |
|
|
90
97
|
| `no_fill: true` | disable pre-fill (perf: skip fill on unwritten cells) |
|
|
@@ -100,7 +107,9 @@ NC::File.create("compressed.nc", netcdf4: true) do |nc|
|
|
|
100
107
|
end
|
|
101
108
|
```
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
zstandard, bzip2 and blosc need libnetcdf 4.9 or later, and every compressor needs libnetcdf built with it: when it is missing, `def_var` raises `NetCDF::Error` rather than writing the data uncompressed. `var.storage_options` reads a variable's settings back in this keyword form. A file compressed with zstandard, bzip2, szip or blosc needs the same filter where it is read: netCDF4-python's wheels, for one, find them only when `HDF5_PLUGIN_PATH` points at libnetcdf's plugin directory.
|
|
111
|
+
|
|
112
|
+
`chunks:`, `deflate:` (a level above 0), `shuffle:`, the other compressors, `fletcher32:` and `endian:` need a NetCDF-4 file; on NetCDF-3, `def_var` raises `ArgumentError` before defining anything. `no_fill:` and `deflate: 0` work on both.
|
|
104
113
|
|
|
105
114
|
**File-wide defaults.** The same kwargs on `File.create` set defaults for every subsequent `def_var` in that file — write once, no need to repeat per variable:
|
|
106
115
|
|
|
@@ -114,7 +123,7 @@ NC::File.create("compressed.nc", netcdf4: true,
|
|
|
114
123
|
end
|
|
115
124
|
```
|
|
116
125
|
|
|
117
|
-
Per-var kwargs on `def_var` always override the file-wide default (including `deflate: 0` / `shuffle: false` to explicitly turn off).
|
|
126
|
+
Per-var kwargs on `def_var` always override the file-wide default (including `deflate: 0` / `shuffle: false` to explicitly turn off). Compression — the compressors and `shuffle` — is taken whole from one place: the kwargs if they name any compressor, else a template's settings if it has one, else the file-wide defaults. So `zstandard: 3` on a file created with `deflate: 4` gives zstandard alone, and `deflate: 0` drops a default `shuffle:` too. A template variable with no compression gets the file-wide default, as a new variable does. These defaults last only while the file created by `create` is open: libnetcdf has no file-level compression setting, so `File.open` takes none, and a variable defined after reopening the file gets none of them.
|
|
118
127
|
|
|
119
128
|
Add attributes after the fact with `Var#def_att`:
|
|
120
129
|
|
|
@@ -125,38 +134,42 @@ nc["temp"].def_att("long_name", "air temperature")
|
|
|
125
134
|
|
|
126
135
|
## Inheriting from an existing file (`template:`)
|
|
127
136
|
|
|
128
|
-
The most common workflow — "read one NetCDF, transform, write another with the same skeleton" —
|
|
137
|
+
The most common workflow — "read one NetCDF, transform, write another with the same skeleton" — needs only one keyword argument. Every `def_var` accepts a `template:` argument that supplies its definition (type, dims, attributes) from an existing `Var` (or from a `File` / `Group` by same-name lookup):
|
|
129
138
|
|
|
130
139
|
```ruby
|
|
131
140
|
NC::File.open("in.nc") do |src|
|
|
132
141
|
NC::File.create("out.nc") do |dst|
|
|
133
142
|
dst.def_dim("lat", 3)
|
|
134
|
-
dst.def_var("lat", template: src["lat"]) # copy definition
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
dst.def_var("lat", template: src
|
|
138
|
-
|
|
139
|
-
dst.def_var("lat",
|
|
143
|
+
dst.def_var("lat", template: src["lat"]) # copy the definition
|
|
144
|
+
|
|
145
|
+
# or any one of:
|
|
146
|
+
# dst.def_var("lat", template: src) # look up "lat" in src
|
|
147
|
+
# dst.def_var("latitude", template: src["lat"]) # rename on copy
|
|
148
|
+
# dst.def_var("lat", template: src["lat"], # override an attribute
|
|
149
|
+
# units: "degrees_north")
|
|
150
|
+
# dst.def_var("lat", :double, template: src["lat"]) # override the type
|
|
140
151
|
end
|
|
141
152
|
end
|
|
142
153
|
```
|
|
143
154
|
|
|
144
|
-
Positional args (`type` / `dims`) and kwargs override what the template provides.
|
|
155
|
+
When both files are NetCDF-4, the template's storage settings — chunk sizes, compression, shuffle, fletcher32 — come along too; its chunk sizes are copied only when the new variable's dimensions have the template's lengths, and libnetcdf chooses them otherwise. Positional args (`type` / `dims`) and kwargs override what the template provides, storage options included. `dims:` and `type:` are also accepted as keyword arguments when mixing with `template:` reads better than positional:
|
|
145
156
|
|
|
146
157
|
```ruby
|
|
158
|
+
dst.def_dim("latitude", 3)
|
|
147
159
|
dst.def_var("lat", template: src["lat"], dims: ["latitude"])
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
`copy_var`
|
|
162
|
+
`copy_var` copies a variable's definition, as above, and its values with it, which suits coordinate variables that pass through unchanged. The dimensions have to exist in the destination first: copy them with `copy_dim`, or start the file with `File.create(template:)`. Attribute and storage options (`units:`, `deflate:`, ...) override the source's, as with `def_var`.
|
|
151
163
|
|
|
152
164
|
```ruby
|
|
165
|
+
dst.copy_dim("lat", from: src) # the dimension first
|
|
153
166
|
dst.copy_var(src["lat"]) # same name, values included
|
|
154
167
|
dst.copy_var(src["lat"], name: "latitude") # rename
|
|
155
168
|
dst.copy_var("lat", from: src) # look up by name
|
|
156
169
|
%w[time lat lon].each { |n| dst.copy_var(n, from: src) }
|
|
157
170
|
```
|
|
158
171
|
|
|
159
|
-
The
|
|
172
|
+
The stored values are copied unchanged, so a packed variable, or one with a `_FillValue`, is copied with no rounding drift. For a copy that goes through the codec, define the variable with `def_var(template:)` and write `dst[name] = src[name].to_ca` yourself.
|
|
160
173
|
|
|
161
174
|
`copy_dim` copies a dimension (including the unlimited flag) but not its coordinate variable:
|
|
162
175
|
|
|
@@ -183,7 +196,7 @@ end
|
|
|
183
196
|
|
|
184
197
|
| Kwarg | Meaning |
|
|
185
198
|
|---|---|
|
|
186
|
-
| `template: src` | inherit dims, atts
|
|
199
|
+
| `template: src` | inherit dims, atts and format (a NetCDF-4 template makes the new file NetCDF-4) |
|
|
187
200
|
| `dims: true` (default) | copy all template dims |
|
|
188
201
|
| `dims: false` | copy no dims |
|
|
189
202
|
| `dims: %w[time lat]` | copy just these dims |
|
|
@@ -210,13 +223,13 @@ NC::File.open("reference.nc") do |ref|
|
|
|
210
223
|
dst.def_var("time", :int, ["time"],
|
|
211
224
|
units: "hours since #{window.origin}", calendar: "standard")
|
|
212
225
|
dst.def_var("temp", :float, ["time","lat","lon"], _FillValue: -999.9)
|
|
213
|
-
window.
|
|
226
|
+
window.each_slab { |t, slab| dst["temp", t, nil, nil] = slab }
|
|
214
227
|
end
|
|
215
228
|
end
|
|
216
229
|
end
|
|
217
230
|
```
|
|
218
231
|
|
|
219
|
-
With `atomic: true` on top,
|
|
232
|
+
With `atomic: true` on top, an exception mid-loop leaves neither a partial file nor a stale one (a killed process can leave a `*.tmp` file behind) — `Dir["out_*.nc"]` tells the truth about what completed. See the [cookbook](07_cookbook.md#9-generate-a-batch-of-files-from-a-single-reference) for a full example.
|
|
220
233
|
|
|
221
234
|
## Defining global attributes
|
|
222
235
|
|
|
@@ -236,7 +249,7 @@ nc.def_att("history", "Created #{Time.now}")
|
|
|
236
249
|
nc["temp"] = CArray.float32(1, 73, 144) { |i, j, k| some_data(i, j, k) }
|
|
237
250
|
```
|
|
238
251
|
|
|
239
|
-
|
|
252
|
+
Every fixed dimension must match the CArray's shape; an unlimited one grows to the array's length. A scalar fills the whole variable (`nc["v"] = 7`).
|
|
240
253
|
|
|
241
254
|
### Hyperslab write
|
|
242
255
|
|
|
@@ -251,8 +264,6 @@ nc["temp", 5..14, nil, nil] = ten_days # ten_days.shape == [10, 73, 144]
|
|
|
251
264
|
nc["temp", 0, 0, 0] = 273.15
|
|
252
265
|
```
|
|
253
266
|
|
|
254
|
-
Under the hood the index dispatches to `nc_put_var1` (single point), `nc_put_vara` (contiguous slab), or `nc_put_vars` (strided slab).
|
|
255
|
-
|
|
256
267
|
### Unlimited-dimension streaming
|
|
257
268
|
|
|
258
269
|
An unlimited dimension grows to fit whatever you write:
|
|
@@ -263,7 +274,7 @@ NC::File.create("stream.nc") do |nc|
|
|
|
263
274
|
nc.def_dim("station", 10)
|
|
264
275
|
nc.def_var("obs", :double, ["time", "station"])
|
|
265
276
|
|
|
266
|
-
# Write day
|
|
277
|
+
# Write day by day; libnetcdf grows the time dimension.
|
|
267
278
|
100.times do |day|
|
|
268
279
|
nc["obs", day, nil] = daily_observations(day) # shape [10]
|
|
269
280
|
end
|
|
@@ -274,7 +285,7 @@ You don't need to declare the eventual size — writing beyond the current lengt
|
|
|
274
285
|
|
|
275
286
|
### Writing with a mask
|
|
276
287
|
|
|
277
|
-
|
|
288
|
+
When the file's `Conventions` attribute names CF or COARDS, the codec handles masks. On a variable with a `_FillValue` attribute, `UNDEF` cells are written as the fill value, and read back as `UNDEF` (see [Codecs](04_codec.md)):
|
|
278
289
|
|
|
279
290
|
```ruby
|
|
280
291
|
NC::File.create("masked.nc") do |nc|
|
|
@@ -288,21 +299,21 @@ NC::File.create("masked.nc") do |nc|
|
|
|
288
299
|
end
|
|
289
300
|
```
|
|
290
301
|
|
|
291
|
-
Writing a masked CArray to a variable
|
|
302
|
+
Writing a masked CArray to a variable with neither `_FillValue` nor `missing_value` raises — the gem refuses to invent a sentinel silently. Without a CF or COARDS `Conventions` attribute (or a codec given with `codec:`), a masked write raises too, so define `Conventions` before the first masked write.
|
|
292
303
|
|
|
293
304
|
## Type-matched writes and libnetcdf coercion
|
|
294
305
|
|
|
295
|
-
You can
|
|
306
|
+
You can write any numeric type; libnetcdf converts it to the on-disk type:
|
|
296
307
|
|
|
297
308
|
```ruby
|
|
298
309
|
nc.def_var("count", :int, ["time"])
|
|
299
|
-
nc["count"] = CArray.int64(
|
|
310
|
+
nc["count"] = CArray.int64(24).seq # OK, libnetcdf narrows int64 -> int32
|
|
300
311
|
|
|
301
312
|
nc.def_var("temp", :float, ["time"])
|
|
302
|
-
nc["temp"] = CArray.float64(
|
|
313
|
+
nc["temp"] = CArray.float64(24).seq # OK, libnetcdf narrows double -> float
|
|
303
314
|
```
|
|
304
315
|
|
|
305
|
-
If a value would overflow the target type, libnetcdf raises. When you're writing physical values into a **packed** (scale/offset) variable, the codec divides by `scale_factor`,
|
|
316
|
+
A float value written to an integer variable is rounded to the nearest integer (`1.7` → `2`), by an array write and a point write alike. If a value would overflow the target type, libnetcdf raises; that includes one that rounding takes past the end of the range (`127.6` into a byte is `128`). When you're writing physical values into a **packed** (scale/offset) variable, the codec subtracts `add_offset`, divides by `scale_factor`, and rounds to the target integer type before the write — see the [Codecs chapter](04_codec.md).
|
|
306
317
|
|
|
307
318
|
## Modifying an existing file
|
|
308
319
|
|
|
@@ -316,13 +327,13 @@ end
|
|
|
316
327
|
|
|
317
328
|
`File.open(:write)` opens for modification. Adding a new dimension or variable is possible too — `File` re-enters define mode as needed.
|
|
318
329
|
|
|
319
|
-
Adding a
|
|
330
|
+
Adding a variable to an existing NetCDF-3 file can make libnetcdf rewrite the file, which is slow for a large one. A NetCDF-4 file takes new variables without a rewrite.
|
|
320
331
|
|
|
321
332
|
## Zarr stores
|
|
322
333
|
|
|
323
|
-
libnetcdf 4.8
|
|
334
|
+
If your libnetcdf (4.8 or later) was built with NCZarr — `nc-config --has-nczarr` says — it can read and write Zarr stores. A Zarr store is a directory, not one file: each chunk of a variable is a file of its own, and the metadata is in small JSON files.
|
|
324
335
|
|
|
325
|
-
The gem has no separate API for it.
|
|
336
|
+
The gem has no separate API for it. Pass a URL where a path would go, and give the format in its `#mode=` part:
|
|
326
337
|
|
|
327
338
|
```ruby
|
|
328
339
|
url = "file:///data/t.zarr#mode=zarr,file"
|
|
@@ -339,15 +350,13 @@ NC::File.open(url) { |nc| nc["t"][] } # decoded as from a .nc fi
|
|
|
339
350
|
NC::File.open(url, :write) { |nc| nc["t", 0, nil] = row }
|
|
340
351
|
```
|
|
341
352
|
|
|
342
|
-
- `mode=zarr` reads and writes plain Zarr, the form other tools such as xarray use.
|
|
343
|
-
- `file` puts the store in a local directory.
|
|
344
|
-
- The `#mode=` part is required.
|
|
345
|
-
|
|
346
|
-
Dimensions, attributes, groups, chunking, compression, the codec and hyperslab reads and writes all work as for a NetCDF-4 file. User-defined types do not.
|
|
353
|
+
- `mode=zarr` reads and writes plain Zarr, the form other tools such as xarray use. `mode=nczarr` also writes NetCDF's own metadata, such as shared dimensions, into the store.
|
|
354
|
+
- `file` puts the store in a local directory. `s3` puts it in Amazon S3, when libnetcdf was built with S3 support.
|
|
355
|
+
- The `#mode=` part is required. A plain path ending in `.zarr` creates an ordinary NetCDF file.
|
|
347
356
|
|
|
348
|
-
|
|
357
|
+
Dimensions, attributes, groups, chunking, compression, the codec and hyperslab reads and writes all work as for a NetCDF-4 file. User-defined types do not.
|
|
349
358
|
|
|
350
|
-
|
|
359
|
+
`atomic:` and `clobber: false` / `clobber: :skip` need a file path, so `File.create` raises `ArgumentError` when they come with a URL. To keep an existing store, check for its directory yourself before calling `create`.
|
|
351
360
|
|
|
352
361
|
## A complete CF-compliant example
|
|
353
362
|
|
|
@@ -413,6 +422,6 @@ NC::File.open("cf_demo.nc") do |nc|
|
|
|
413
422
|
t[0] # => #<CATime::Element 2022-01-01T00:00:00Z>
|
|
414
423
|
|
|
415
424
|
temp = nc["temp"].to_ca # decoded; fill sentinel -> mask
|
|
416
|
-
temp.mean #
|
|
425
|
+
temp.mean # about 273.5 K
|
|
417
426
|
end
|
|
418
427
|
```
|