carray-netcdf 1.0.13 → 2.0.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 +79 -0
- data/CONTRIBUTING.md +75 -0
- data/LICENSE +21 -0
- data/README.md +108 -2
- data/bin/ncheader +223 -0
- data/bin/ncvar +160 -0
- data/carray-netcdf.gemspec +37 -19
- data/docs/00_introduction.md +72 -0
- data/docs/01_getting_started.md +114 -0
- data/docs/02_reading.md +216 -0
- data/docs/03_writing.md +418 -0
- data/docs/04_codec.md +373 -0
- data/docs/05_groups.md +200 -0
- data/docs/06_layer1.md +160 -0
- data/docs/07_cookbook.md +345 -0
- data/docs/08_cheatsheet.md +326 -0
- data/docs/09_cli.md +79 -0
- data/lib/carray/netcdf/codec.rb +536 -0
- data/lib/carray/netcdf/dim.rb +67 -0
- data/lib/carray/netcdf/file.rb +354 -0
- data/lib/carray/netcdf/group.rb +517 -0
- data/lib/carray/netcdf/var.rb +539 -0
- data/lib/carray/netcdf/version.rb +5 -0
- data/lib/carray/netcdf.rb +287 -0
- data/lib/carray-netcdf.rb +3 -1
- metadata +80 -25
- data/API.md +0 -270
- data/Rakefile +0 -12
- data/bin/ncdef2yaml +0 -60
- data/examples/test.nc +0 -0
- data/examples/test.rb +0 -41
- data/examples/time.rb +0 -11
- data/ext/extconf.rb +0 -11
- data/ext/rb_netcdflib.c +0 -2674
- data/lib/io/netcdf.rb +0 -848
data/docs/03_writing.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Writing files
|
|
2
|
+
|
|
3
|
+
Writing a file is a schema followed by data, and the gem keeps the two phases in the order NetCDF wants:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
NC::File.create(path) do |nc|
|
|
7
|
+
nc.def_dim("time", :unlimited) # the schema: dimensions,
|
|
8
|
+
nc.def_var("temp", :float, [...]) # variables,
|
|
9
|
+
nc.def_att("Conventions", "CF-1.7") # and attributes
|
|
10
|
+
|
|
11
|
+
nc["temp"] = ca # then the data
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`create` opens the file in define mode, so the definitions come first. You do not have to leave define mode yourself: the first data write does it, and a later definition puts the file back. The block closes the file when it ends.
|
|
16
|
+
|
|
17
|
+
What you assign is a CArray, and the codec encodes it on the way in — the reverse of the read path, so a CArray of physical values can be stored packed, and a masked entry becomes the variable's `_FillValue`.
|
|
18
|
+
|
|
19
|
+
## Creating
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "carray-netcdf"
|
|
23
|
+
NC = CA::NetCDF
|
|
24
|
+
|
|
25
|
+
# Classic NetCDF-3
|
|
26
|
+
NC::File.create("out.nc") do |nc|
|
|
27
|
+
# ... define dims/vars/atts, then write data
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# NetCDF-4 (needed for groups, compression, unlimited multi-D, etc.)
|
|
31
|
+
NC::File.create("out.nc", netcdf4: true) do |nc|
|
|
32
|
+
# ...
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Refuse to overwrite an existing file
|
|
36
|
+
NC::File.create("out.nc", clobber: false)
|
|
37
|
+
|
|
38
|
+
# Atomic write: publish only on success. Bytes go to a same-directory
|
|
39
|
+
# tmp file; a successful close renames tmp -> "out.nc" (POSIX-atomic).
|
|
40
|
+
# Any exception from the block unlinks the tmp; the target file is
|
|
41
|
+
# untouched (an earlier version, if any, survives intact).
|
|
42
|
+
NC::File.create("out.nc", atomic: true) do |nc|
|
|
43
|
+
# ... define + write. On raise: tmp cleaned up, "out.nc" NOT created /
|
|
44
|
+
# overwritten. On success: "out.nc" appears atomically at end of block.
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**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
|
+
|
|
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
|
+
## Defining dimensions
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
nc.def_dim("time", :unlimited) # unlimited (grows as you write)
|
|
56
|
+
nc.def_dim("lat", 73) # fixed length
|
|
57
|
+
nc.def_dim("lon", 144)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`:unlimited` (or the string `"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
|
+
|
|
62
|
+
## Defining variables
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
66
|
+
units: "K",
|
|
67
|
+
long_name: "air temperature",
|
|
68
|
+
_FillValue: -999.9,
|
|
69
|
+
valid_range: [200.0, 350.0])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- 1st arg: variable name (String).
|
|
73
|
+
- 2nd arg: type — a Symbol from `:byte :short :int :int64 :float :double :ubyte :ushort :uint :uint64 :char` (plus the CArray-style aliases `:int8 :uint8 :int16 :uint16 :int32 :uint32 :float32 :float64`).
|
|
74
|
+
- 3rd arg: list of dimension names (Strings). An empty list makes a scalar.
|
|
75
|
+
- Keyword args: attributes to attach at define time. Any Ruby-side Numeric, String, or Array is fine.
|
|
76
|
+
|
|
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 enforce this). The gem handles that for you: it detects those attribute names and writes them via typed `nc_put_att_*`, so `_FillValue: -999.9` on a `:float` var becomes `nc_put_att_float`, not the default `nc_put_att_double`. Other attributes go through the generic `nc_put_att` (which infers a reasonable NetCDF type from the Ruby value).
|
|
78
|
+
|
|
79
|
+
**NetCDF-4 storage / filter options.** `def_var` recognizes these keyword arguments alongside the attributes (only effective on a NetCDF-4 file):
|
|
80
|
+
|
|
81
|
+
| Kwarg | Meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `chunks: [1, 73, 144]` | chunk sizes (`NC_CHUNKED`) |
|
|
84
|
+
| `chunks: :contiguous` | force `NC_CONTIGUOUS` |
|
|
85
|
+
| `chunks: :compact` | `NC_COMPACT` (small variables only) |
|
|
86
|
+
| `deflate: 4` | zlib compression level 1..9 (0 / omitted / `nil` = off) |
|
|
87
|
+
| `shuffle: true` | shuffle filter (usually paired with deflate) |
|
|
88
|
+
| `fletcher32: true` | per-chunk fletcher32 checksum |
|
|
89
|
+
| `endian: :little / :big / :native` | on-disk byte order |
|
|
90
|
+
| `no_fill: true` | disable pre-fill (perf: skip fill on unwritten cells) |
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
NC::File.create("compressed.nc", netcdf4: true) do |nc|
|
|
94
|
+
nc.def_dim("time", :unlimited); nc.def_dim("lat", 73); nc.def_dim("lon", 144)
|
|
95
|
+
nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
96
|
+
units: "K",
|
|
97
|
+
chunks: [1, 73, 144],
|
|
98
|
+
deflate: 4,
|
|
99
|
+
shuffle: true)
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
These get passed through to `nc_def_var_chunking` / `nc_def_var_deflate` / `nc_def_var_fletcher32` / `nc_def_var_endian` / `nc_def_var_fill` and take effect immediately. On a NetCDF-3 file, any of them causes libnetcdf to raise; that raise propagates unchanged.
|
|
104
|
+
|
|
105
|
+
**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
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
NC::File.create("compressed.nc", netcdf4: true,
|
|
109
|
+
deflate: 4, shuffle: true) do |nc|
|
|
110
|
+
nc.def_dim("time", :unlimited); nc.def_dim("lat", 73); nc.def_dim("lon", 144)
|
|
111
|
+
nc.def_var("temp", :float, ["time", "lat", "lon"]) # inherits deflate=4, shuffle=true
|
|
112
|
+
nc.def_var("wind", :float, ["time", "lat", "lon"]) # same
|
|
113
|
+
nc.def_var("mask", :byte, ["lat", "lon"], deflate: 0) # per-var wins: no compression
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Per-var kwargs on `def_var` always override the file-wide default (including `deflate: 0` / `shuffle: false` to explicitly turn off). Note that "file-wide compression" is a convention: libnetcdf has no file-level compression attribute — internally we just call `nc_def_var_deflate` on each new variable with the file's default. If you `def_var` in a second session (after re-open), the earlier file default is *not* remembered and applies only within the create block.
|
|
118
|
+
|
|
119
|
+
Add attributes after the fact with `Var#def_att`:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
nc.def_var("temp", :float, ["time", "lat", "lon"], units: "K")
|
|
123
|
+
nc["temp"].def_att("long_name", "air temperature")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Inheriting from an existing file (`template:`)
|
|
127
|
+
|
|
128
|
+
The most common workflow — "read one NetCDF, transform, write another with the same skeleton" — is a single kwarg away. 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
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
NC::File.open("in.nc") do |src|
|
|
132
|
+
NC::File.create("out.nc") do |dst|
|
|
133
|
+
dst.def_dim("lat", 3)
|
|
134
|
+
dst.def_var("lat", template: src["lat"]) # copy definition
|
|
135
|
+
dst.def_var("lat", template: src) # look up "lat" in src
|
|
136
|
+
dst.def_var("latitude", template: src["lat"]) # rename on copy
|
|
137
|
+
dst.def_var("lat", template: src["lat"], # override attributes
|
|
138
|
+
units: "arc_degrees_north")
|
|
139
|
+
dst.def_var("lat", :double, template: src["lat"]) # override type
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Positional args (`type` / `dims`) and kwargs override what the template provides. `dims:` and `type:` are also accepted as keyword arguments when mixing with `template:` reads better than positional:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
dst.def_var("lat", template: src["lat"], dims: ["latitude"])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`copy_var` combines the definition copy above with a bit-identical raw data copy — perfect for coordinate variables that pass through unchanged:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
dst.copy_var(src["lat"]) # same name, values included
|
|
154
|
+
dst.copy_var(src["lat"], name: "latitude") # rename
|
|
155
|
+
dst.copy_var("lat", from: src) # look up by name
|
|
156
|
+
%w[time lat lon].each { |n| dst.copy_var(n, from: src) }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The data goes through the source's `raw` (no decode) and layer 1 (no encode), so the on-disk bytes match exactly — a codec-mediated variable (scale/offset, `_FillValue`) round-trips without any floating-point drift. If you want a codec-mediated copy, drop back to `def_var(template:)` and write `dst[name] = src[name].to_ca` yourself.
|
|
160
|
+
|
|
161
|
+
`copy_dim` copies a dimension (including the unlimited flag) but not its coordinate variable:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
dst.copy_dim(src.dim("time")) # unlimited stays unlimited
|
|
165
|
+
dst.copy_dim("lat", from: src)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**File-level template.** `File.create` accepts `template:` for whole-file skeleton inheritance — dims, global attributes, and (if given in `vars:`) coordinate variables are copied before your block runs:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
NC::File.open("in.nc") do |src|
|
|
172
|
+
NC::File.create("out.nc",
|
|
173
|
+
template: src,
|
|
174
|
+
vars: %w[time lat lon]) do |dst|
|
|
175
|
+
# dims, global atts, and coord vars are already there
|
|
176
|
+
dst.def_var("result", :float, ["time","lat","lon"], units: "1")
|
|
177
|
+
dst["result"] = compute(src["temp"].to_ca)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`template:` also works with `dims:`, `atts:`, and format inheritance:
|
|
183
|
+
|
|
184
|
+
| Kwarg | Meaning |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `template: src` | inherit dims, atts, and format (netcdf-4 upgrades) |
|
|
187
|
+
| `dims: true` (default) | copy all template dims |
|
|
188
|
+
| `dims: false` | copy no dims |
|
|
189
|
+
| `dims: %w[time lat]` | copy just these dims |
|
|
190
|
+
| `atts: true` (default) | copy all global atts |
|
|
191
|
+
| `atts: false` | copy no atts |
|
|
192
|
+
| `atts: { "history" => "..." }` | copy all, then apply Hash overrides |
|
|
193
|
+
| `vars: %w[time lat lon]` | after dims/atts, `copy_var` each of these |
|
|
194
|
+
|
|
195
|
+
Explicit `netcdf4:` always wins over template inheritance.
|
|
196
|
+
|
|
197
|
+
## Idempotent batch creation (`clobber: :skip`)
|
|
198
|
+
|
|
199
|
+
For pipelines that generate one output file per input chunk, `clobber:` also accepts `:skip` — if the target already exists, the block is not called and `File.create` returns `nil`:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
NC::File.open("reference.nc") do |ref|
|
|
203
|
+
time_windows.each do |window|
|
|
204
|
+
NC::File.create("out_#{window.tag}.nc",
|
|
205
|
+
template: ref,
|
|
206
|
+
vars: %w[lat lon],
|
|
207
|
+
clobber: :skip,
|
|
208
|
+
atomic: true) do |dst|
|
|
209
|
+
# only runs for windows not already produced
|
|
210
|
+
dst.def_var("time", :int, ["time"],
|
|
211
|
+
units: "hours since #{window.origin}", calendar: "standard")
|
|
212
|
+
dst.def_var("temp", :float, ["time","lat","lon"], _FillValue: -999.9)
|
|
213
|
+
window.slabs { |t, slab| dst["temp", t, nil, nil] = slab }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
With `atomic: true` on top, a mid-loop crash leaves neither a partial file nor a stale one — `Dir["out_*.nc"]` tells the truth about what completed. See the [cookbook batch chapter](07_cookbook.md) for a full example.
|
|
220
|
+
|
|
221
|
+
## Defining global attributes
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
nc.def_att("Conventions", "CF-1.7")
|
|
225
|
+
nc.def_att("title", "Reanalysis subset")
|
|
226
|
+
nc.def_att("history", "Created #{Time.now}")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`def_att` on the File writes to `NC_GLOBAL`. On a `Group` it writes to that group's global-attribute set (see [Groups](05_groups.md)).
|
|
230
|
+
|
|
231
|
+
## Writing data
|
|
232
|
+
|
|
233
|
+
### Whole-variable write
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
nc["temp"] = CArray.float32(1, 73, 144) { |i, j, k| some_data(i, j, k) }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The CArray shape must match the variable's shape. Non-contiguous CArrays (views) are automatically materialized to a contiguous buffer before the write.
|
|
240
|
+
|
|
241
|
+
### Hyperslab write
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# Write one time step
|
|
245
|
+
nc["temp", 0, nil, nil] = one_slice # one_slice.shape == [73, 144]
|
|
246
|
+
|
|
247
|
+
# Write ten time steps starting at t=5
|
|
248
|
+
nc["temp", 5..14, nil, nil] = ten_days # ten_days.shape == [10, 73, 144]
|
|
249
|
+
|
|
250
|
+
# Write a single point
|
|
251
|
+
nc["temp", 0, 0, 0] = 273.15
|
|
252
|
+
```
|
|
253
|
+
|
|
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
|
+
### Unlimited-dimension streaming
|
|
257
|
+
|
|
258
|
+
An unlimited dimension grows to fit whatever you write:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
NC::File.create("stream.nc") do |nc|
|
|
262
|
+
nc.def_dim("time", :unlimited)
|
|
263
|
+
nc.def_dim("station", 10)
|
|
264
|
+
nc.def_var("obs", :double, ["time", "station"])
|
|
265
|
+
|
|
266
|
+
# Write day-by-day. netcdf grows the time dimension automatically.
|
|
267
|
+
100.times do |day|
|
|
268
|
+
nc["obs", day, nil] = daily_observations(day) # shape [10]
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
You don't need to declare the eventual size — writing beyond the current length grows the unlimited dimension. Every fixed-size dimension must match the write.
|
|
274
|
+
|
|
275
|
+
### Writing with a mask
|
|
276
|
+
|
|
277
|
+
Layer 2 handles masks for you via the codec. On a variable with a `_FillValue` attribute, `UNDEF` cells are encoded to the fill sentinel on write; on read, fill cells decode back to `UNDEF` (see [Codecs](04_codec.md)):
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
NC::File.create("masked.nc") do |nc|
|
|
281
|
+
nc.def_dim("n", 5)
|
|
282
|
+
nc.def_var("v", :float, ["n"], _FillValue: -999.9)
|
|
283
|
+
nc.def_att("Conventions", "CF-1.7")
|
|
284
|
+
|
|
285
|
+
ca = CA_FLOAT([1.0, 2.0, 3.0, 4.0, 5.0])
|
|
286
|
+
ca[2] = UNDEF
|
|
287
|
+
nc["v"] = ca # cell [2] is written as -999.9 on disk
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Writing a masked CArray to a variable without a `_FillValue` attribute raises — the gem refuses to invent a sentinel silently.
|
|
292
|
+
|
|
293
|
+
## Type-matched writes and libnetcdf coercion
|
|
294
|
+
|
|
295
|
+
You can hand libnetcdf any numeric type; it converts on the way out to match the on-disk type:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
nc.def_var("count", :int, ["time"])
|
|
299
|
+
nc["count"] = CArray.int64(...) # OK, libnetcdf narrows int64 -> int32
|
|
300
|
+
|
|
301
|
+
nc.def_var("temp", :float, ["time"])
|
|
302
|
+
nc["temp"] = CArray.float64(...) # OK, libnetcdf narrows double -> float
|
|
303
|
+
```
|
|
304
|
+
|
|
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`, subtracts `add_offset`, and rounds to the target integer type before the write — see the [Codecs chapter](04_codec.md).
|
|
306
|
+
|
|
307
|
+
## Modifying an existing file
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
NC::File.open("data.nc", :write) do |nc|
|
|
311
|
+
nc["temp", 0, nil, nil] = new_slice # overwrite a slab
|
|
312
|
+
nc.def_att("history", "updated #{Time.now}") # new global attribute
|
|
313
|
+
nc["temp"].def_att("comment", "revised on ingest")
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`File.open(:write)` opens for modification. Adding a new dimension or variable is possible too — `File` re-enters define mode as needed.
|
|
318
|
+
|
|
319
|
+
Adding a new variable to an existing NetCDF-3 file rewrites the file layout (a libnetcdf constraint, not ours). NetCDF-4 files (`netcdf4: true`) are more flexible.
|
|
320
|
+
|
|
321
|
+
## Zarr stores
|
|
322
|
+
|
|
323
|
+
libnetcdf 4.8 and later can read and write Zarr. libnetcdf calls this support NCZarr. 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
|
+
|
|
325
|
+
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
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
url = "file:///data/t.zarr#mode=zarr,file"
|
|
329
|
+
|
|
330
|
+
NC::File.create(url, netcdf4: true) do |nc|
|
|
331
|
+
nc.def_dim("y", 2)
|
|
332
|
+
nc.def_dim("x", 3)
|
|
333
|
+
nc.def_var("t", :short, %w[y x], scale_factor: 0.5, _FillValue: -1)
|
|
334
|
+
nc.def_att("Conventions", "CF-1.8")
|
|
335
|
+
nc["t"] = ca
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
NC::File.open(url) { |nc| nc["t"][] } # decoded as from a .nc file
|
|
339
|
+
NC::File.open(url, :write) { |nc| nc["t", 0, nil] = row }
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- `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.
|
|
343
|
+
- `file` puts the store in a local directory. `s3` puts it in Amazon S3, when libnetcdf was built with S3 support.
|
|
344
|
+
- The `#mode=` part is required. A plain path ending in `.zarr` creates an ordinary HDF5 file.
|
|
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.
|
|
347
|
+
|
|
348
|
+
`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`.
|
|
349
|
+
|
|
350
|
+
This needs a libnetcdf built with NCZarr. `nc-config --has-nczarr` says whether yours was.
|
|
351
|
+
|
|
352
|
+
## A complete CF-compliant example
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
require "carray-netcdf"
|
|
356
|
+
NC = CA::NetCDF
|
|
357
|
+
|
|
358
|
+
NC::File.create("cf_demo.nc") do |nc|
|
|
359
|
+
# Dimensions
|
|
360
|
+
nc.def_dim("time", :unlimited)
|
|
361
|
+
nc.def_dim("lat", 73)
|
|
362
|
+
nc.def_dim("lon", 144)
|
|
363
|
+
|
|
364
|
+
# Coordinate variables
|
|
365
|
+
nc.def_var("time", :int, ["time"],
|
|
366
|
+
standard_name: "time",
|
|
367
|
+
long_name: "time",
|
|
368
|
+
units: "hours since 2022-01-01T00:00:00Z",
|
|
369
|
+
calendar: "standard")
|
|
370
|
+
nc.def_var("lat", :float, ["lat"],
|
|
371
|
+
standard_name: "latitude",
|
|
372
|
+
long_name: "latitude",
|
|
373
|
+
units: "degrees_north")
|
|
374
|
+
nc.def_var("lon", :float, ["lon"],
|
|
375
|
+
standard_name: "longitude",
|
|
376
|
+
long_name: "longitude",
|
|
377
|
+
units: "degrees_east")
|
|
378
|
+
|
|
379
|
+
# Data variable
|
|
380
|
+
nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
381
|
+
standard_name: "air_temperature",
|
|
382
|
+
long_name: "surface air temperature",
|
|
383
|
+
units: "K",
|
|
384
|
+
_FillValue: -999.9,
|
|
385
|
+
valid_range: [200.0, 350.0])
|
|
386
|
+
|
|
387
|
+
# Global attributes
|
|
388
|
+
nc.def_att("Conventions", "CF-1.7")
|
|
389
|
+
nc.def_att("title", "Synthetic CF demo")
|
|
390
|
+
nc.def_att("history", "Created #{Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')}")
|
|
391
|
+
|
|
392
|
+
# Coordinate data
|
|
393
|
+
nc["lat"] = CArray.float32(73) { |i| -90.0 + i * 2.5 }
|
|
394
|
+
nc["lon"] = CArray.float32(144) { |i| i * 2.5 }
|
|
395
|
+
nc["time"] = CArray.time_series("2022-01-01T00:00:00Z", count: 24, unit: :h)
|
|
396
|
+
|
|
397
|
+
# Data — 24 hourly slices
|
|
398
|
+
24.times do |t|
|
|
399
|
+
nc["temp", t, nil, nil] = CArray.float32(73, 144) { |i, j|
|
|
400
|
+
273.15 + 20.0 * Math.sin(i * 0.1 + t * 0.2) + Math.cos(j * 0.05)
|
|
401
|
+
}
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Round-tripping this file:
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
NC::File.open("cf_demo.nc") do |nc|
|
|
410
|
+
nc.att("Conventions") # => "CF-1.7"
|
|
411
|
+
|
|
412
|
+
t = nc["time"].to_ca # => CATime, hour resolution
|
|
413
|
+
t[0] # => #<CATime::Element 2022-01-01T00:00:00Z>
|
|
414
|
+
|
|
415
|
+
temp = nc["temp"].to_ca # decoded; fill sentinel -> mask
|
|
416
|
+
temp.mean # centered around 273-290 K
|
|
417
|
+
end
|
|
418
|
+
```
|