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/07_cookbook.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cookbook
|
|
2
2
|
|
|
3
|
-
End-to-end scenarios
|
|
3
|
+
End-to-end scenarios, from writing a file through reading, copying and converting one to working with data larger than memory. Some recipes read a file an earlier one wrote, and say which; names such as `time_windows` or `process` stand for your own data and code.
|
|
4
4
|
|
|
5
5
|
## 1. Write a CF-1.7 hourly temperature file
|
|
6
6
|
|
|
@@ -61,21 +61,14 @@ NC::File.open("hourly_temp.nc") do |nc|
|
|
|
61
61
|
|
|
62
62
|
temp = nc["temp"].to_ca # decoded (fill -> mask if present)
|
|
63
63
|
temp.shape # => [24, 73, 144]
|
|
64
|
-
temp.mean #
|
|
65
|
-
end
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## 2. Read the third time step of a CF file, mask-aware
|
|
64
|
+
temp.mean # => about 271.8
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
slab = nc["temp"][2, nil, nil] # rank-2 CArray, shape [73, 144]
|
|
73
|
-
slab.count_masked # 0 unless the file has fill cells here
|
|
74
|
-
slab_at_equator = slab[36, nil] # rank-1 CArray, shape [144]
|
|
66
|
+
slab = nc["temp"][2, nil, nil] # the third time step only, shape [73, 144]
|
|
67
|
+
slab.count_masked # 0 unless the file has fill cells there
|
|
75
68
|
end
|
|
76
69
|
```
|
|
77
70
|
|
|
78
|
-
##
|
|
71
|
+
## 2. Pack a physical field into a short
|
|
79
72
|
|
|
80
73
|
Storing a physical range that fits comfortably in `short` saves half the space of `float`. CF's `scale_factor` / `add_offset` handle it:
|
|
81
74
|
|
|
@@ -117,70 +110,11 @@ NC::File.open("packed.nc", codec: [:cf, mask_valid_range: true]) do |nc|
|
|
|
117
110
|
end
|
|
118
111
|
```
|
|
119
112
|
|
|
120
|
-
##
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
require "carray-netcdf"
|
|
124
|
-
NC = CA::NetCDF
|
|
113
|
+
## 3. Append time steps to an unlimited-dimension file
|
|
125
114
|
|
|
126
|
-
|
|
127
|
-
# Compute the daily mean without ever holding the whole array.
|
|
128
|
-
|
|
129
|
-
means = CArray.float64(10000)
|
|
130
|
-
|
|
131
|
-
NC::File.open("/big.nc") do |nc|
|
|
132
|
-
temp = nc["temp"]
|
|
133
|
-
n_time = temp.shape[0]
|
|
134
|
-
|
|
135
|
-
# For zero-allocation streaming, drop to layer 1. Layer 2's hyperslab
|
|
136
|
-
# read allocates a fresh CArray per step; layer 1 lets us reuse a buffer.
|
|
137
|
-
vid = ::NetCDF.nc_inq_varid(nc.ncid, "temp")
|
|
138
|
-
buf = CArray.float32(1, *temp.shape[1..]) # [1, lat, lon]
|
|
139
|
-
|
|
140
|
-
n_time.times do |k|
|
|
141
|
-
NC.nc_get_vara(nc.ncid, vid, [k, 0, 0], [1, *temp.shape[1..]], buf)
|
|
142
|
-
means[k] = buf.mean # raw physical bytes
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
If your file has `_FillValue` or `scale_factor`, layer 1 doesn't apply them — you'd need to either drop that particular streaming loop into layer 2 (which allocates per slab but is one line: `means[k] = nc["temp"][k, nil, nil].mean`), or apply the transforms yourself.
|
|
148
|
-
|
|
149
|
-
## 4b. Generate a batch of files from a single reference
|
|
150
|
-
|
|
151
|
-
The classic "read one, write many" pipeline: hold a reference NetCDF open, loop over some units of work, and emit one output per iteration that inherits the reference's skeleton (dims, global atts, coordinate vars) and only diverges in the payload. `File.create(template:, vars:)` collapses the boilerplate; `clobber: :skip` + `atomic: true` make the loop restartable.
|
|
115
|
+
Adds one hour to the file from recipe 1:
|
|
152
116
|
|
|
153
117
|
```ruby
|
|
154
|
-
NC::File.open("reference.nc") do |ref|
|
|
155
|
-
time_windows.each do |window| # e.g. one 24h chunk per iteration
|
|
156
|
-
NC::File.create("out_#{window.tag}.nc",
|
|
157
|
-
template: ref, # dims + atts + format inherited
|
|
158
|
-
vars: %w[lat lon], # coord vars: full bit-identical copy
|
|
159
|
-
clobber: :skip, # already-produced files are left alone
|
|
160
|
-
atomic: true) do |dst|
|
|
161
|
-
dst.def_var("time", :int, ["time"],
|
|
162
|
-
units: "hours since #{window.origin}", calendar: "standard")
|
|
163
|
-
dst.def_var("temp", :float, ["time","lat","lon"], _FillValue: -999.9)
|
|
164
|
-
dst["time"] = window.timestamps
|
|
165
|
-
window.each_slab { |t, slab| dst["temp", t, nil, nil] = slab }
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Why the combination matters:
|
|
172
|
-
|
|
173
|
-
- **`template: ref`** — dims, `Conventions`, `title`, and any custom global attributes carry over. Output inherits the NetCDF-4 format if the reference is netcdf-4 (so groups / deflate work), unless `netcdf4:` is set explicitly.
|
|
174
|
-
- **`vars: %w[lat lon]`** — coordinate variables that don't change between outputs come along for the ride, byte-identical (no codec on either side). Downstream diff / dedup tools won't see spurious floating-point drift.
|
|
175
|
-
- **`clobber: :skip`** — reruns pick up where a previous run stopped; finished files are not touched. Combines cleanly with `atomic:`.
|
|
176
|
-
- **`atomic: true`** — a mid-loop crash leaves neither a half-written output nor a stale one. `Dir["out_*.nc"]` reflects exactly what completed successfully.
|
|
177
|
-
|
|
178
|
-
For read-side workflows that consume the output, the split is clean: either the file is fully there (produced by a completed `create` block), or it is not there yet. No corrupt-file recovery logic in downstream code.
|
|
179
|
-
|
|
180
|
-
## 5. Append time steps to an unlimited-dimension file
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
# Existing file created with time = :unlimited.
|
|
184
118
|
NC::File.open("hourly_temp.nc", :write) do |nc|
|
|
185
119
|
cur = nc.dim("time").length # e.g. 24
|
|
186
120
|
|
|
@@ -191,7 +125,7 @@ NC::File.open("hourly_temp.nc", :write) do |nc|
|
|
|
191
125
|
end
|
|
192
126
|
```
|
|
193
127
|
|
|
194
|
-
##
|
|
128
|
+
## 4. Group your model by subsystem (NetCDF-4)
|
|
195
129
|
|
|
196
130
|
```ruby
|
|
197
131
|
NC::File.create("model.nc", netcdf4: true) do |nc|
|
|
@@ -213,10 +147,10 @@ NC::File.create("model.nc", netcdf4: true) do |nc|
|
|
|
213
147
|
units: "hours since 2024-01-01T00:00:00Z", calendar: "standard")
|
|
214
148
|
|
|
215
149
|
# Write some data
|
|
216
|
-
|
|
217
|
-
nc["time"] = CArray.time_series("2024-01-01T00:00:00Z", count:
|
|
150
|
+
steps = 6
|
|
151
|
+
nc["time"] = CArray.time_series("2024-01-01T00:00:00Z", count: steps, unit: :h)
|
|
218
152
|
|
|
219
|
-
|
|
153
|
+
steps.times do |t|
|
|
220
154
|
ocean["temperature", t, nil] = CArray.float32(50) { |z| 25.0 - z * 0.4 }
|
|
221
155
|
ocean["salinity", t, nil] = CArray.float32(50) { |z| 35.0 + z * 0.01 }
|
|
222
156
|
atm["pressure", t, nil] = CArray.float32(30) { |z| 1013.0 * Math.exp(-z / 8.0) }
|
|
@@ -236,13 +170,33 @@ NC::File.open("model.nc") do |nc|
|
|
|
236
170
|
end
|
|
237
171
|
```
|
|
238
172
|
|
|
239
|
-
##
|
|
173
|
+
## 5. Select a time range by date
|
|
174
|
+
|
|
175
|
+
Find the steps between two instants on the time coordinate, then read only those. With the file from recipe 1:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
NC::File.open("hourly_temp.nc") do |nc|
|
|
179
|
+
t = nc["time"].to_ca # CATime
|
|
180
|
+
from = CArray.time(["2022-01-01T06:00:00Z"], unit: :h)[0]
|
|
181
|
+
upto = CArray.time(["2022-01-01T11:00:00Z"], unit: :h)[0]
|
|
182
|
+
|
|
183
|
+
k = ((t >= from) & (t <= upto)).where # => [6, 7, 8, 9, 10, 11]
|
|
184
|
+
morning = nc["temp"][k.min..k.max, nil, nil] # reads those 6 steps only
|
|
185
|
+
morning.shape # => [6, 73, 144]
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The comparison works on instants, so `from` and `upto` may be at any resolution, a day as well as an hour. Reading the range `k.min..k.max` rather than `k` itself keeps the read a single block, which matters when the steps are many.
|
|
190
|
+
|
|
191
|
+
## 6. Inspect a mystery file quickly
|
|
240
192
|
|
|
241
193
|
```ruby
|
|
242
194
|
NC::File.open("mystery.nc") do |nc|
|
|
243
|
-
|
|
195
|
+
format = NetCDF.constants.grep(/\ANC_FORMAT_/).find { |c| NetCDF.const_get(c) == nc.format }
|
|
196
|
+
codec = nc.codec.is_a?(Module) ? nc.codec : nc.codec.class # Identity is a module
|
|
197
|
+
puts "format: #{format}"
|
|
244
198
|
puts "conventions: #{nc.att('Conventions') || '(none)'}"
|
|
245
|
-
puts "codec: #{
|
|
199
|
+
puts "codec: #{codec.name.split('::').last}"
|
|
246
200
|
puts
|
|
247
201
|
|
|
248
202
|
puts "dimensions:"
|
|
@@ -267,27 +221,141 @@ NC::File.open("mystery.nc") do |nc|
|
|
|
267
221
|
end
|
|
268
222
|
```
|
|
269
223
|
|
|
270
|
-
|
|
224
|
+
For the whole header as CDL, JSON or YAML, the `ncheader` command does this from the shell (see [Command-line tools](09_cli.md)). To see what the codec does to one variable, compare `v.to_ca` with `v.raw` (see [Seeing what the codec changed](04_codec.md#seeing-what-the-codec-changed)).
|
|
225
|
+
|
|
226
|
+
## 7. Cut a region out into a new file
|
|
227
|
+
|
|
228
|
+
Copy a latitude band of the file from recipe 1, with each variable's definition and attributes, through the codec: values are decoded on the way out and encoded again on the way in, so masks and packing carry over.
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
NC::File.open("hourly_temp.nc") do |src|
|
|
232
|
+
lat = src["lat"].to_ca
|
|
233
|
+
j = ((lat >= 20) & (lat <= 50)).where # the rows of the band
|
|
234
|
+
rows = j.min..j.max
|
|
235
|
+
|
|
236
|
+
NC::File.create("band.nc", template: src, dims: %w[time lon], vars: %w[time lon]) do |dst|
|
|
237
|
+
dst.def_dim("lat", rows.size)
|
|
238
|
+
dst.def_var("lat", template: src["lat"])
|
|
239
|
+
dst.def_var("temp", template: src["temp"])
|
|
240
|
+
dst["lat"] = src["lat"][rows]
|
|
241
|
+
src["time"].shape[0].times do |t| # one time step at a time
|
|
242
|
+
dst["temp", t, nil, nil] = src["temp"][t, rows, nil]
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`File.create(template:)` brings the global attributes and, with `dims:` / `vars:`, the dimensions and coordinate variables that stay the same; `def_var(template:)` takes each changed variable's type and attributes. Writing a step at a time keeps one slab in memory.
|
|
249
|
+
|
|
250
|
+
## 8. Convert a NetCDF-3 file to compressed NetCDF-4
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
NC::File.open("hourly_temp.nc") do |src|
|
|
254
|
+
NC::File.create("hourly_temp4.nc", template: src, format: :netcdf4) do |dst|
|
|
255
|
+
src.vars.each_key do |name|
|
|
256
|
+
dst.copy_var(name, from: src, deflate: 4, shuffle: true)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
NC::File.open("hourly_temp4.nc") { |nc| nc["temp"].storage_options }
|
|
262
|
+
# => {chunks: [1, 73, 144], deflate: 4, shuffle: true}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`template:` brings the dimensions and global attributes, and `copy_var` each variable with its stored values unchanged; the kwargs set the compression. On this file that halves its size. Add `chunks:` to choose the chunking yourself, or use `zstandard:` (libnetcdf 4.9 and later) for faster compression; see [Writing files](03_writing.md).
|
|
271
266
|
|
|
272
|
-
|
|
267
|
+
## 9. Generate a batch of files from a single reference
|
|
268
|
+
|
|
269
|
+
Hold a reference file open, and write one output per unit of work, each with the reference's dimensions, global attributes and coordinate variables. `File.create(template:, vars:)` copies those; `clobber: :skip` and `atomic: true` let the loop be run again after a failure. Here the reference's `time` dimension is unlimited, so each output can hold its own number of steps, and each `window` gives `tag`, `origin`, `timestamps` (a CATime) and `each_slab`, which yields a time index and a `[lat, lon]` CArray.
|
|
273
270
|
|
|
274
271
|
```ruby
|
|
275
|
-
NC::File.open("
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
NC::File.open("reference.nc") do |ref|
|
|
273
|
+
time_windows.each do |window| # e.g. one 24h chunk per iteration
|
|
274
|
+
NC::File.create("out_#{window.tag}.nc",
|
|
275
|
+
template: ref, # dims + atts + format inherited
|
|
276
|
+
vars: %w[lat lon], # coord vars: full bit-identical copy
|
|
277
|
+
clobber: :skip, # already-produced files are left alone
|
|
278
|
+
atomic: true) do |dst|
|
|
279
|
+
dst.def_var("time", :int, ["time"],
|
|
280
|
+
units: "hours since #{window.origin}", calendar: "standard")
|
|
281
|
+
dst.def_var("temp", :float, ["time","lat","lon"], _FillValue: -999.9)
|
|
282
|
+
dst["time"] = window.timestamps
|
|
283
|
+
window.each_slab { |t, slab| dst["temp", t, nil, nil] = slab }
|
|
284
|
+
end
|
|
285
|
+
end
|
|
278
286
|
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Why the combination matters:
|
|
279
290
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
- **`template: ref`** — dims, `Conventions`, `title`, and any custom global attributes carry over. The output has the reference's format, unless `format:` or `netcdf4:` is given.
|
|
292
|
+
- **`vars: %w[lat lon]`** — coordinate variables that are the same in every output are copied with their stored values unchanged, so they compare equal byte for byte.
|
|
293
|
+
- **`clobber: :skip`** — a rerun skips the outputs that already exist and writes the rest.
|
|
294
|
+
- **`atomic: true`** — an exception mid-loop leaves no half-written output, so `Dir["out_*.nc"]` lists only completed files. A killed process can leave a `*.tmp` file behind.
|
|
295
|
+
|
|
296
|
+
## 10. Stream a large variable one slab at a time
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
require "carray-netcdf"
|
|
300
|
+
NC = CA::NetCDF
|
|
301
|
+
|
|
302
|
+
# big.nc has a variable temp[time, lat, lon] with many time steps.
|
|
303
|
+
# Compute the mean of each time step without holding the whole array.
|
|
304
|
+
|
|
305
|
+
NC::File.open("big.nc") do |nc|
|
|
306
|
+
temp = nc["temp"]
|
|
307
|
+
n_time = temp.shape[0]
|
|
308
|
+
means = CArray.float64(n_time)
|
|
309
|
+
|
|
310
|
+
# The low-level API reads each step into the same buffer, so nothing is
|
|
311
|
+
# allocated per step.
|
|
312
|
+
buf = CArray.float32(1, *temp.shape[1..]) # [1, lat, lon]
|
|
313
|
+
|
|
314
|
+
n_time.times do |k|
|
|
315
|
+
NC.nc_get_vara(nc.ncid, temp.varid, [k, 0, 0], [1, *temp.shape[1..]], buf)
|
|
316
|
+
means[k] = buf.mean # stored values, not decoded
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The low-level API applies no codec, so `buf` holds the stored values. If the variable has a `_FillValue` or `scale_factor`, decode each step with `temp.codec.decode(temp, buf)`, or read it with `temp[k, nil, nil]`, which decodes but allocates a new array per step.
|
|
322
|
+
|
|
323
|
+
## 11. Read in one thread, compute in another
|
|
324
|
+
|
|
325
|
+
libnetcdf runs without the GVL, so the main thread can compute on one slab while a reader thread reads the next:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
require "carray-netcdf"
|
|
329
|
+
NC = CA::NetCDF
|
|
330
|
+
|
|
331
|
+
queue = SizedQueue.new(4) # at most 4 slabs held at once
|
|
332
|
+
|
|
333
|
+
reader = Thread.new do
|
|
334
|
+
NC::File.open("big.nc") do |nc| # this File is used by this thread only
|
|
335
|
+
temp = nc["temp"]
|
|
336
|
+
temp.shape[0].times { |k| queue << temp[k, nil, nil] }
|
|
337
|
+
end
|
|
338
|
+
ensure
|
|
339
|
+
queue << nil # no more slabs, even if the read failed
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
means = []
|
|
343
|
+
while slab = queue.pop # each slab is handed over once it has been read
|
|
344
|
+
means << slab.mean
|
|
284
345
|
end
|
|
346
|
+
reader.join # re-raises an error from the reader thread
|
|
285
347
|
```
|
|
286
348
|
|
|
287
|
-
|
|
349
|
+
This stays within what is safe with threads (see [Threads and Ractors](00_introduction.md#threads-and-ractors)):
|
|
350
|
+
|
|
351
|
+
- **One thread owns the file.** The `File` and its `Var` are opened, used and closed in the reader thread. To read several files at once, give each thread its own `File`; the calls into libnetcdf still run one at a time.
|
|
352
|
+
- **A CArray changes hands only after its read.** A slab goes into the queue once it has been read, and the reader does not touch it again.
|
|
353
|
+
|
|
354
|
+
## 12. Mask fill values without unpacking
|
|
355
|
+
|
|
356
|
+
A codec of your own can apply only part of CF. This one turns `_FillValue` into a mask and back, and leaves `scale_factor` and `add_offset` alone, so the values stay packed:
|
|
288
357
|
|
|
289
358
|
```ruby
|
|
290
|
-
# A minimal codec that only masks _FillValue but skips scale/offset/time.
|
|
291
359
|
class OnlyFillCodec
|
|
292
360
|
def decode(var, ca)
|
|
293
361
|
fill = var.att("_FillValue") or return ca
|
|
@@ -304,42 +372,15 @@ class OnlyFillCodec
|
|
|
304
372
|
end
|
|
305
373
|
end
|
|
306
374
|
|
|
307
|
-
NC::File.
|
|
308
|
-
|
|
309
|
-
|
|
375
|
+
NC::File.create("fill_only.nc", codec: OnlyFillCodec.new) do |nc|
|
|
376
|
+
nc.def_dim("n", 3)
|
|
377
|
+
nc.def_var("v", :short, ["n"], scale_factor: 0.5, _FillValue: -1)
|
|
378
|
+
nc["v"] = CA_SHORT([1, UNDEF, 3]) # the masked element is written as -1
|
|
310
379
|
end
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
The codec object just has to respond to `decode(var, ca)` and `encode(var, ca)`. See the [Codecs chapter](04_codec.md) for the full interface and how to register a codec globally.
|
|
314
380
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
```ruby
|
|
318
|
-
def cdl_summary(nc)
|
|
319
|
-
puts "netcdf {"
|
|
320
|
-
puts "dimensions:"
|
|
321
|
-
nc.dims.each { |n, d|
|
|
322
|
-
len = d.unlimited? ? "UNLIMITED" : d.length.to_s
|
|
323
|
-
puts " #{n} = #{len};"
|
|
324
|
-
}
|
|
325
|
-
puts "variables:"
|
|
326
|
-
nc.vars.each { |n, v|
|
|
327
|
-
dims = v.dims.map(&:name).join(", ")
|
|
328
|
-
puts " #{v.data_type} #{n}(#{dims});"
|
|
329
|
-
v.atts.each { |k, val|
|
|
330
|
-
puts " #{n}:#{k} = #{val.inspect};"
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
puts "// global attributes:"
|
|
334
|
-
nc.atts.each { |k, v| puts " :#{k} = #{v.inspect};" }
|
|
335
|
-
nc.groups.each { |gname, g|
|
|
336
|
-
puts
|
|
337
|
-
puts "group: #{gname} {"
|
|
338
|
-
cdl_summary(g)
|
|
339
|
-
puts "} // group #{gname}"
|
|
340
|
-
}
|
|
341
|
-
puts "}"
|
|
381
|
+
NC::File.open("fill_only.nc", codec: OnlyFillCodec.new) do |nc|
|
|
382
|
+
nc["v"].to_ca.to_a # => [1, UNDEF, 3] (still packed)
|
|
342
383
|
end
|
|
343
|
-
|
|
344
|
-
NC::File.open("mystery.nc") { |nc| cdl_summary(nc) }
|
|
345
384
|
```
|
|
385
|
+
|
|
386
|
+
The codec object just has to respond to `decode(var, ca)` and `encode(var, ca)`. See the [Codecs chapter](04_codec.md) for the full interface and how to register a codec, so that files whose `Conventions` it claims pick it up.
|