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/04_codec.md
CHANGED
|
@@ -6,36 +6,25 @@ A **codec** turns raw on-disk values into Ruby-side values on read and back agai
|
|
|
6
6
|
- **`CF`** — the CF Conventions value transforms.
|
|
7
7
|
- **`COARDS`** — CF's predecessor; functionally identical for the value transforms this codec touches.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Every read and write through a `Var` goes through the file's codec — a whole variable, a slab or a single point — so you rarely call a codec yourself.
|
|
10
10
|
|
|
11
11
|
## What CF actually does (and doesn't)
|
|
12
12
|
|
|
13
13
|
| CF attribute | Codec does |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `_Unsigned` | Read: bit-reinterpret signed on-disk type to its unsigned counterpart (`int8 → uint8`, etc.).
|
|
16
|
-
| `_FillValue`, `missing_value` | Read: mask (`UNDEF`).
|
|
17
|
-
| `scale_factor`, `add_offset` | Read: `packed × scale + offset
|
|
18
|
-
| `valid_range`, `valid_min`, `valid_max` | Read (opt-in): out-of-range → `UNDEF`.
|
|
19
|
-
| `units` = `"<u> since <ref>"` (+ `calendar`) | Read: `CATime`.
|
|
15
|
+
| `_Unsigned` | Read: bit-reinterpret signed on-disk type to its unsigned counterpart (`int8 → uint8`, etc.). Write: reverse. Applied before the others, so they see unsigned values. |
|
|
16
|
+
| `_FillValue`, `missing_value` | Read: mask (`UNDEF`). Write: `UNDEF` → fill sentinel. If `_Unsigned` is set, the attribute value is reinterpreted to unsigned space before comparison. |
|
|
17
|
+
| `scale_factor`, `add_offset` | Read: `packed × scale + offset`, in float32 when the attributes are float32 (as xarray does). Write: `round((v − offset) / scale)` into var type. |
|
|
18
|
+
| `valid_range`, `valid_min`, `valid_max` | Read (opt-in): out-of-range → `UNDEF`. Write: no effect. |
|
|
19
|
+
| `units` = `"<u> since <ref>"` (+ `calendar`) | Read: `CATime`. Write: `CATime` → count since ref (a fraction only into a float variable). |
|
|
20
20
|
| `units` = anything else (`"m s-1"`, `"K"`, `"kg m-3"`, …) | Raw attribute, **no UDUNITS conversion**. |
|
|
21
21
|
| everything else (`standard_name`, `grid_mapping`, `cell_methods`, `coordinates`, `ancillary_variables`, …) | Raw attribute, no interpretation. |
|
|
22
22
|
|
|
23
|
-
The
|
|
24
|
-
|
|
25
|
-
### Contract for codec methods
|
|
26
|
-
|
|
27
|
-
Every codec method takes `(var, ca)` and only that. A codec:
|
|
28
|
-
|
|
29
|
-
- Reads attributes off `var` (own attributes only).
|
|
30
|
-
- Transforms the CArray `ca` (own values only).
|
|
31
|
-
- **Does NOT** walk to `var.file` to reach other variables or global state. Ruby cannot enforce this structurally, but doing so is a contract violation and puts the codec on the wrong side of the boundary.
|
|
32
|
-
- **Does NOT** rely on `var.dims` beyond reading their names (for documentation purposes, if at all) — again, that leaves the variable's frame of reference.
|
|
33
|
-
|
|
34
|
-
The dispatch that picks a codec at File.open time DOES look at the whole file (specifically the `Conventions` attribute) — that's a one-shot selection, not a per-transform operation, so it stays on the right side of the boundary.
|
|
23
|
+
The codec works on one variable's values. Coordinate systems, grid mappings, cell methods and standard names stay as plain attributes (`var.att("grid_mapping")` works); see [Beyond this gem](00_introduction.md#beyond-this-gem).
|
|
35
24
|
|
|
36
25
|
## Codec selection
|
|
37
26
|
|
|
38
|
-
By default, `File.open`
|
|
27
|
+
By default, a file's codec follows its `Conventions` global attribute. `File.open` looks at it when the file is opened; a file being created looks at it on its first read or write, so define `Conventions` before writing data.
|
|
39
28
|
|
|
40
29
|
```ruby
|
|
41
30
|
NC::File.open("cf_file.nc") do |nc|
|
|
@@ -51,14 +40,15 @@ NC::File.open("no_conventions.nc") do |nc|
|
|
|
51
40
|
end
|
|
52
41
|
```
|
|
53
42
|
|
|
54
|
-
|
|
43
|
+
The rules:
|
|
55
44
|
|
|
56
45
|
- `Conventions: "COARDS"` (or combos like `"COARDS/CF-1.0"`) → `COARDS`.
|
|
57
46
|
- `Conventions: "CF-1.7"` / `"CF-1.8"` / `"CF 1.7"` / `"CF"` → `CF`.
|
|
58
47
|
- When the whole string matches neither, each of its tokens (split on spaces, commas and slashes) is tried, so `"ACDD-1.3, CF-1.8"` → `CF`.
|
|
59
|
-
- Anything else, including missing → `Identity`.
|
|
48
|
+
- Anything else, including missing → `Identity`. A file with no `Conventions` whose variables carry `_FillValue`, `scale_factor` or time units — what xarray and netCDF4-python write — warns once and suggests `codec: :cf`.
|
|
49
|
+
- Matching is case-sensitive: `"cf-1.8"` is not recognised.
|
|
60
50
|
|
|
61
|
-
|
|
51
|
+
Choose the codec yourself with `codec:`, on `File.open` or `File.create`:
|
|
62
52
|
|
|
63
53
|
```ruby
|
|
64
54
|
NC::File.open("data.nc", codec: :identity) # raw only
|
|
@@ -84,7 +74,7 @@ end
|
|
|
84
74
|
NC::File.open("fill.nc") do |nc|
|
|
85
75
|
v = nc["v"].to_ca
|
|
86
76
|
v.is_masked[1] # => true (decoded to mask)
|
|
87
|
-
v[1]
|
|
77
|
+
nc["v"][1] # => UNDEF (a point read is decoded too)
|
|
88
78
|
v.count_masked # => 1
|
|
89
79
|
end
|
|
90
80
|
|
|
@@ -96,12 +86,12 @@ end
|
|
|
96
86
|
|
|
97
87
|
If a variable has **both** `_FillValue` and `missing_value`, both sentinels are masked. `missing_value` may be a scalar or an array (multiple sentinels).
|
|
98
88
|
|
|
99
|
-
Writing `UNDEF` to a variable
|
|
89
|
+
Writing `UNDEF` to a variable with neither `_FillValue` nor `missing_value` raises:
|
|
100
90
|
|
|
101
91
|
```ruby
|
|
102
92
|
ca = CA_SHORT([1, 2, 3])
|
|
103
93
|
ca[1] = UNDEF
|
|
104
|
-
nc["v"] = ca # ArgumentError: no _FillValue to encode
|
|
94
|
+
nc["v"] = ca # ArgumentError: variable 'v' has masked values but no _FillValue to encode them
|
|
105
95
|
```
|
|
106
96
|
|
|
107
97
|
## `scale_factor` and `add_offset`
|
|
@@ -135,7 +125,7 @@ NC::File.open("packed.nc", codec: :identity) do |nc|
|
|
|
135
125
|
end
|
|
136
126
|
```
|
|
137
127
|
|
|
138
|
-
**Rounding is required.** A bare cast to an integer variable would truncate (`
|
|
128
|
+
**Rounding is required.** A bare cast to an integer variable would truncate (`0.29 / 0.01 == 28.999999999999996` → `28`); the codec rounds. Reading needs no rounding.
|
|
139
129
|
|
|
140
130
|
Interaction with `_FillValue`: **fill is judged in packed space, before unpacking.** So `_FillValue: -32768` on a packed short is compared against the raw short values, not against the unpacked physical values.
|
|
141
131
|
|
|
@@ -213,14 +203,20 @@ end
|
|
|
213
203
|
|
|
214
204
|
### N-of-a-unit intervals: `"5 days since ..."`
|
|
215
205
|
|
|
216
|
-
|
|
206
|
+
A count in units such as `"5 days since 2024-06-15"` reads as a CATime at the base unit, a day here, so a stored `1` is 2024-06-20. A write into an integer variable has to land on the 5-day grid; an instant between its steps raises `ArgumentError` (a float variable takes the fraction):
|
|
217
207
|
|
|
218
208
|
```ruby
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
209
|
+
NC::File.create("five.nc") do |nc|
|
|
210
|
+
nc.def_att("Conventions", "CF-1.8")
|
|
211
|
+
nc.def_dim("time", 3)
|
|
212
|
+
nc.def_var("time", :int, ["time"], units: "5 days since 2024-06-15")
|
|
213
|
+
nc["time"] = CA_INT64([0, 5, 10]).time(unit: :D, origin: "2024-06-15")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
NC::File.open("five.nc") do |nc|
|
|
217
|
+
nc["time"].raw.to_a # => [0, 1, 2]
|
|
218
|
+
nc["time"][1].to_s # => "2024-06-20"
|
|
219
|
+
end
|
|
224
220
|
```
|
|
225
221
|
|
|
226
222
|
### Float time coordinates
|
|
@@ -234,20 +230,22 @@ units: "days since ..." with float values 0.0, 0.5, 1.25 -> CATime at 1 h: +0 h
|
|
|
234
230
|
|
|
235
231
|
Only `year` and `month` fractions are rounded to the nearest whole tick, since those units have no fixed length.
|
|
236
232
|
|
|
237
|
-
On write, an instant finer than the units' interval is written as a fraction into a float variable (`01:30` into `"days since ..."` is `0.0625`). An integer variable can't hold one, so the write raises `ArgumentError` instead of truncating.
|
|
233
|
+
On write, an instant finer than the units' interval is written as a fraction into a float variable (`01:30` into `"days since ..."` is `0.0625`). An integer variable can't hold one, so the write raises `ArgumentError` instead of truncating. `months since` and `years since` take only instants on their own grid, into either kind of variable, since a month has no fixed length; a monthly CATime written to `"days since ..."` lands on each month's first day.
|
|
234
|
+
|
|
235
|
+
The reference time need not fall on the unit's grid. `"days since 2000-01-01 12:00:00"`, a time zone (`+09:00`) or fractional seconds are worked through in the finer unit they need, so a stored `0` reads as the reference instant itself. A short year in the UDUNITS style (`"days since 1-1-1"`) is year 1, not 2001. A `months since` or `years since` reference that is not the head of a month or year is the one case read as raw values, with a warning.
|
|
238
236
|
|
|
239
237
|
### Calendars
|
|
240
238
|
|
|
241
239
|
The codec handles these calendar attribute values:
|
|
242
240
|
|
|
243
|
-
- `standard`, `gregorian`, `proleptic_gregorian
|
|
244
|
-
- Anything else — `noleap`, `365_day`, `all_leap`, `366_day`, `360_day`, `julian`, `none` — is unsupported. The codec warns once per
|
|
241
|
+
- `standard`, `gregorian`, `proleptic_gregorian` (in any case), or missing → CATime (proleptic Gregorian, UTC).
|
|
242
|
+
- Anything else — `noleap`, `365_day`, `all_leap`, `366_day`, `360_day`, `julian`, `none` — is unsupported. The codec warns once per calendar value in each opened file and returns raw values unchanged. Writing a `CATime` to a variable with an unsupported calendar raises.
|
|
245
243
|
|
|
246
|
-
|
|
244
|
+
`standard` and `gregorian` are CF's mixed calendar: Julian before 1582-10-15, Gregorian from then on. A reference time before the reform (the NCEP-style `"hours since 1-1-1 00:00:0.0"`) is read as the Julian date it is, so every instant from the reform on comes out as cftime and UDUNITS give it. Instants before the reform are shown as proleptic Gregorian, with a warning, and writing one into such units raises; use `calendar: "proleptic_gregorian"` for those, and another library for non-standard calendars.
|
|
247
245
|
|
|
248
246
|
## Identity codec
|
|
249
247
|
|
|
250
|
-
Skip all transforms
|
|
248
|
+
Skip all transforms and get the stored values:
|
|
251
249
|
|
|
252
250
|
```ruby
|
|
253
251
|
NC::File.open("data.nc", codec: :identity) do |nc|
|
|
@@ -257,11 +255,11 @@ NC::File.open("data.nc", codec: :identity) do |nc|
|
|
|
257
255
|
end
|
|
258
256
|
```
|
|
259
257
|
|
|
260
|
-
Useful for
|
|
258
|
+
Useful for copying unknown files unchanged, or for seeing what a producer actually wrote.
|
|
261
259
|
|
|
262
260
|
## COARDS codec
|
|
263
261
|
|
|
264
|
-
COARDS (1995) is CF's predecessor
|
|
262
|
+
COARDS (1995) is CF's predecessor, and a COARDS file is also a CF file, so the COARDS codec is a subclass of CF and applies every transform above. Only the `Conventions` strings it is chosen for differ.
|
|
265
263
|
|
|
266
264
|
```ruby
|
|
267
265
|
NC::File.open("coards_file.nc") do |nc|
|
|
@@ -271,11 +269,11 @@ NC::File.open("coards_file.nc") do |nc|
|
|
|
271
269
|
end
|
|
272
270
|
```
|
|
273
271
|
|
|
274
|
-
|
|
272
|
+
To tell COARDS files apart, test `is_a?(NC::Codec::COARDS)` before `is_a?(NC::Codec::CF)`, because a COARDS codec is also a CF one.
|
|
275
273
|
|
|
276
274
|
`Conventions` strings that route to COARDS:
|
|
277
|
-
- `"COARDS"`
|
|
278
|
-
-
|
|
275
|
+
- A string that starts with `COARDS` (`"COARDS"`, `"COARDS/CF-1.0"`).
|
|
276
|
+
- A string that starts with neither `COARDS` nor `CF`, when one of its words (split at spaces, commas and slashes) starts with `COARDS` — even if another word names CF (`"ACDD-1.3, COARDS"`).
|
|
279
277
|
|
|
280
278
|
COARDS predates the `calendar` attribute; a COARDS time variable without an explicit calendar lands on the same proleptic-Gregorian axis as CF `"standard"` (identical for post-1582 dates).
|
|
281
279
|
|
|
@@ -285,27 +283,38 @@ The codec interface is minimal:
|
|
|
285
283
|
|
|
286
284
|
```ruby
|
|
287
285
|
class MyCodec
|
|
288
|
-
# Optional:
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# Required: the two transforms.
|
|
295
|
-
#
|
|
296
|
-
|
|
297
|
-
|
|
286
|
+
# Optional: return true for the Conventions strings this codec should be
|
|
287
|
+
# chosen for. It sees the string only.
|
|
288
|
+
def self.claims?(conventions)
|
|
289
|
+
conventions.start_with?("MYCONV")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Required: the two transforms.
|
|
293
|
+
def decode(var, ca) # stored values -> what a read returns
|
|
294
|
+
ca
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def encode(var, ca) # what is written -> the values to store
|
|
298
|
+
ca
|
|
299
|
+
end
|
|
298
300
|
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Both methods get one variable and its values. Read what you need from the attributes of `var` itself, and leave other variables alone: a codec runs on every read and write, down to a single point.
|
|
299
304
|
|
|
305
|
+
Register it so that files whose `Conventions` it claims pick it up:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
300
308
|
CA::NetCDF::Codec.register(MyCodec)
|
|
301
309
|
```
|
|
302
310
|
|
|
303
|
-
|
|
311
|
+
A registered class is created with `.new`, with no arguments, when a file's `Conventions` matches it; later registrations are tried first. The `codec:` option of `File.open` and `File.create` accepts:
|
|
304
312
|
|
|
305
313
|
- `:auto` (default) — match by `Conventions`; fallback `Identity`
|
|
306
314
|
- `:identity` / `false` / `nil` → `Identity`
|
|
307
315
|
- `:cf`, `:coards` → the named codec with defaults
|
|
308
316
|
- `[:cf, opts]`, `[:coards, opts]` → with keyword options
|
|
317
|
+
- a Class — instantiated with no arguments
|
|
309
318
|
- a `Module` (used as-is, e.g. `Identity`)
|
|
310
319
|
- any object that responds to `#decode(var, ca)` and `#encode(var, ca)`
|
|
311
320
|
|
|
@@ -324,30 +333,21 @@ class OnlyMasksCodec
|
|
|
324
333
|
end
|
|
325
334
|
|
|
326
335
|
NC::File.open("data.nc", codec: OnlyMasksCodec.new) do |nc|
|
|
327
|
-
...
|
|
336
|
+
# ...
|
|
328
337
|
end
|
|
329
338
|
```
|
|
330
339
|
|
|
331
|
-
##
|
|
332
|
-
|
|
333
|
-
Decode (read):
|
|
334
|
-
|
|
335
|
-
1. `decode_unsigned` — `_Unsigned` reinterprets signed on-disk bits to their unsigned counterpart. Runs FIRST so every subsequent comparison (fill, valid_range) sees the numerically-correct value.
|
|
336
|
-
2. `decode_fill` — `_FillValue` / `missing_value` → mask (in unsigned space if `_Unsigned` was set; the attribute value is reinterpreted).
|
|
337
|
-
3. `decode_valid` — `valid_range` / `valid_min` / `valid_max` → mask (opt-in, same space as fill).
|
|
338
|
-
4. `decode_scale` — `packed × scale + offset`.
|
|
339
|
-
5. `decode_time` — `count-since-ref` → `CATime`.
|
|
340
|
-
|
|
341
|
-
Encode (write) runs in reverse:
|
|
340
|
+
## Order of the transforms
|
|
342
341
|
|
|
343
|
-
|
|
344
|
-
2. `encode_scale` — `round((v − offset) / scale)` for integer var types.
|
|
345
|
-
3. `encode_fill` — `UNDEF` → fill sentinel (in the user's unsigned space).
|
|
346
|
-
4. `encode_unsigned` — reinterpret unsigned user values back to signed on-disk bits.
|
|
342
|
+
Reading applies them in this order, and writing in the reverse:
|
|
347
343
|
|
|
348
|
-
|
|
344
|
+
1. `_Unsigned` — signed stored values are read as unsigned.
|
|
345
|
+
2. `_FillValue` / `missing_value` — matching values become masked.
|
|
346
|
+
3. `valid_range` / `valid_min` / `valid_max` — values out of range become masked (opt-in, and on reading only; CF defines no write direction).
|
|
347
|
+
4. `scale_factor` / `add_offset` — packed values are unpacked.
|
|
348
|
+
5. A time `units` — counts become CATime.
|
|
349
349
|
|
|
350
|
-
##
|
|
350
|
+
## Seeing what the codec changed
|
|
351
351
|
|
|
352
352
|
```ruby
|
|
353
353
|
NC::File.open("data.nc") do |nc|
|
|
@@ -355,19 +355,11 @@ NC::File.open("data.nc") do |nc|
|
|
|
355
355
|
v.atts # everything the codec might read
|
|
356
356
|
v.codec # which codec will run
|
|
357
357
|
|
|
358
|
-
raw = v.raw #
|
|
359
|
-
decoded = v.to_ca #
|
|
358
|
+
raw = v.raw # the stored values, not decoded
|
|
359
|
+
decoded = v.to_ca # the same as v.codec.decode(v, v.raw)
|
|
360
360
|
|
|
361
361
|
# diff them cell by cell to see what the codec actually changed
|
|
362
362
|
end
|
|
363
363
|
```
|
|
364
364
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
You can also open the file with `codec: :identity` to skip decoding for every read in the block:
|
|
368
|
-
|
|
369
|
-
```ruby
|
|
370
|
-
NC::File.open("data.nc", codec: :identity) do |nc|
|
|
371
|
-
nc["temp"].to_ca # raw bytes, uniformly for every variable
|
|
372
|
-
end
|
|
373
|
-
```
|
|
365
|
+
`v.raw` takes the same indexes as `v[...]`, so you can compare a slab before and after decoding.
|
data/docs/05_groups.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
NetCDF-4 files can organize their contents in a tree of **groups** — much like directories in a filesystem. Each group has its own dimensions, variables, and global attributes; nested groups form a hierarchy.
|
|
4
4
|
|
|
5
|
-
The classic NetCDF-3 model is a single root group; every file has one, so in this library **every `File`
|
|
5
|
+
The classic NetCDF-3 model is a single root group; every file has one, so in this library **every `File` is a Group** (the root group). Everything you can do on `File` — `def_dim`, `def_var`, `def_att`, `nc[name]`, `nc[name] = ...` — is a `Group` method, plus subgroup navigation.
|
|
6
6
|
|
|
7
7
|
## Creating groups
|
|
8
8
|
|
|
@@ -48,7 +48,7 @@ NC::File.open("model.nc") do |nc|
|
|
|
48
48
|
surface = ocean.group("surface")
|
|
49
49
|
surface.path # => "/ocean/surface"
|
|
50
50
|
|
|
51
|
-
nc.name # => "/" (
|
|
51
|
+
nc.name # => "/" (libnetcdf's name for the root group)
|
|
52
52
|
nc.path # => "/"
|
|
53
53
|
nc.root? # => true
|
|
54
54
|
end
|
|
@@ -135,7 +135,7 @@ nc.group("child").resolve_dim("bogus") # => raises KeyError
|
|
|
135
135
|
|
|
136
136
|
## Per-group attributes
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
In NetCDF-4 each group has its own attributes; `def_att` / `atts` / `att` on a Group read and write that group's:
|
|
139
139
|
|
|
140
140
|
```ruby
|
|
141
141
|
NC::File.create("attrs.nc", netcdf4: true) do |nc|
|
|
@@ -156,7 +156,7 @@ end
|
|
|
156
156
|
|
|
157
157
|
## Codecs are inherited from the root
|
|
158
158
|
|
|
159
|
-
The codec is a whole-file policy — `Conventions` at the root determines which codec applies to every variable, including those in subgroups. `Group#codec` delegates to the File.
|
|
159
|
+
The codec is a whole-file policy — `Conventions` at the root determines which codec applies to every variable, including those in subgroups. A `Conventions` attribute on a subgroup is ignored. `Group#codec` delegates to the File.
|
|
160
160
|
|
|
161
161
|
```ruby
|
|
162
162
|
NC::File.create("cf_groups.nc", netcdf4: true) do |nc|
|
|
@@ -188,7 +188,7 @@ end
|
|
|
188
188
|
| `group.parent` | parent Group, or `nil` for root |
|
|
189
189
|
| `group.root?` | Boolean |
|
|
190
190
|
| `group.file` | the enclosing File (root Group) |
|
|
191
|
-
| `group.ncid` |
|
|
191
|
+
| `group.ncid` | the group's id (`grpid` in the netcdf gem), for use with the low-level API |
|
|
192
192
|
| `group.dims` | `{String => Dim}` — this group only |
|
|
193
193
|
| `group.vars` | `{String => Var}` — this group only |
|
|
194
194
|
| `group.groups` | `{String => Group}` — direct children |
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Low-level API — the `nc_*` functions
|
|
2
|
+
|
|
3
|
+
Everyday code uses the high-level API (`File`, `Var`, ...). The low-level API is the CArray-flavored view of the netcdf gem's `nc_*` functions — same names, same argument order, but numeric buffers are CArrays.
|
|
4
|
+
|
|
5
|
+
Use it when:
|
|
6
|
+
|
|
7
|
+
- You want to work with ncid and varid integers directly, as in C.
|
|
8
|
+
- You want to stream a very large variable into a preallocated buffer (`nc_get_vara` with an explicit buffer = zero allocation per read).
|
|
9
|
+
- You're bridging to code that already speaks the C `nc_*` idiom.
|
|
10
|
+
- You want to skip the codec and everything else the high-level API adds, for a specific operation.
|
|
11
|
+
|
|
12
|
+
## The module
|
|
13
|
+
|
|
14
|
+
`CA::NetCDF` is the low-level API. It `include`s `NetCDF` (the netcdf gem's module) and overrides only the functions that read and write variable data, so that they speak CArray. A class that mixes `CA::NetCDF` in can call every `nc_*` function and `NC_*` constant bare. Called on the module itself, only the overridden get and put functions are public: `NC.nc_get_vara(...)` works, while `NC.nc_open(...)` raises `NoMethodError` — call those on `NetCDF`.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "carray-netcdf"
|
|
18
|
+
NC = CA::NetCDF
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Two idioms:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# (a) call style — module functions
|
|
25
|
+
NC.nc_get_vara(ncid, varid, [0, 0], [3, 4]) # => CArray
|
|
26
|
+
NC::NC_DOUBLE # constants inherited from NetCDF
|
|
27
|
+
|
|
28
|
+
# (b) mixin style — `nc_*` become bare methods in your class
|
|
29
|
+
class Reader
|
|
30
|
+
include CA::NetCDF
|
|
31
|
+
def read_all(ncid, varid) = nc_get_var(ncid, varid) # bare, returns CArray
|
|
32
|
+
def define(ncid, name) = nc_def_dim(ncid, name, 100) # passes through
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What the low-level API overrides vs passes through
|
|
37
|
+
|
|
38
|
+
Overridden (numeric array I/O):
|
|
39
|
+
|
|
40
|
+
- `nc_get_var`, `nc_get_vara`, `nc_get_vars`, `nc_get_varm`
|
|
41
|
+
- `nc_put_var`, `nc_put_vara`, `nc_put_vars`, `nc_put_varm`
|
|
42
|
+
|
|
43
|
+
Everything else — `nc_open`, `nc_create`, `nc_def_dim`, `nc_def_var`, `nc_put_att`, `nc_get_att`, `nc_inq_*`, `nc_def_grp`, `nc_close`, `NC_DOUBLE`, `NC_CLOBBER`, ... — comes straight from the netcdf gem, which the low-level API includes without modifying.
|
|
44
|
+
|
|
45
|
+
## `nc_get_var*` — buffer-less returns a CArray
|
|
46
|
+
|
|
47
|
+
Without a buffer argument, the low-level API allocates a CArray of the requested shape, typed from the variable's on-disk type (`NC_CHAR` as `:uint8`, an enum as its base type), and reads into it:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
ncid = NetCDF.nc_open("grid.nc", NetCDF::NC_NOWRITE) # temp[y, x], a 2-D variable
|
|
51
|
+
vid = NetCDF.nc_inq_varid(ncid, "temp")
|
|
52
|
+
|
|
53
|
+
ca = NC.nc_get_var(ncid, vid) # whole variable -> CArray
|
|
54
|
+
ca = NC.nc_get_vara(ncid, vid, [0, 0], [3, 4]) # hyperslab -> CArray
|
|
55
|
+
ca = NC.nc_get_vars(ncid, vid, [0, 0], [3, 4], [1, 2]) # strided -> CArray
|
|
56
|
+
ca = NC.nc_get_varm(ncid, vid, [0, 0], [3, 4], [1, 1], [4, 1]) # mapped -> CArray
|
|
57
|
+
|
|
58
|
+
NetCDF.nc_close(ncid)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
No codec runs — this is raw, no `_FillValue → UNDEF`, no scale/offset, no CATime. Use the high-level API for those.
|
|
62
|
+
|
|
63
|
+
An `NC_STRING` variable has no CArray type: read it with the netcdf gem's `NetCDF.nc_get_var(ncid, varid)`, which returns Strings. An opaque variable raises `NotImplementedError`; read it into a buffer of your own.
|
|
64
|
+
|
|
65
|
+
## `nc_get_var*` — with a buffer, reads into it
|
|
66
|
+
|
|
67
|
+
Pass an existing CArray and libnetcdf reads into it, so a slab at a time can be streamed with **no allocation per read**:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
ncid = NetCDF.nc_open("big.nc", NetCDF::NC_NOWRITE)
|
|
71
|
+
vid = NetCDF.nc_inq_varid(ncid, "temp")
|
|
72
|
+
|
|
73
|
+
buf = CArray.float32(73, 144) # one time slice
|
|
74
|
+
|
|
75
|
+
365.times do |day|
|
|
76
|
+
NC.nc_get_vara(ncid, vid, [day, 0, 0], [1, 73, 144], buf)
|
|
77
|
+
process(buf) # buf now holds day `day`
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
NetCDF.nc_close(ncid)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The buffer must be one contiguous block of memory: a CArray you allocated, or a contiguous slice of one. A strided view is refused, and so is a buffer too small for the read. Type conversion is handled by libnetcdf — you can read a short variable into a float64 buffer and libnetcdf converts.
|
|
84
|
+
|
|
85
|
+
## `nc_put_var*` — CArray as buffer
|
|
86
|
+
|
|
87
|
+
Symmetric. A contiguous CArray goes through as-is:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
NC.nc_put_vara(ncid, vid, [0, 0], [3, 4], CArray.float32(3, 4).seq!)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A non-contiguous CArray (a view / slice / reference) is materialized to a contiguous entity before the write:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
big = CArray.float32(100, 4).seq!
|
|
97
|
+
NC.nc_put_vara(ncid, vid, [0, 0], [3, 4], big[10..12, nil])
|
|
98
|
+
# big[10..12, nil] is a view of big; it is copied into one block of
|
|
99
|
+
# memory before the write
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A **masked** CArray is refused, since the low-level API applies no convention; filling masked elements is the codec's job in the high-level API. Strip the mask first:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
ca = CA_SHORT([1, 2, 3])
|
|
106
|
+
ca[1] = UNDEF
|
|
107
|
+
NC.nc_put_vara(ncid, vid, [0], [3], ca) # ArgumentError
|
|
108
|
+
NC.nc_put_vara(ncid, vid, [0], [3], ca.strip_mask(-32768)) # OK, fill baked in
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## `nc_get_var1` / `nc_put_var1` — scalar
|
|
112
|
+
|
|
113
|
+
These are the netcdf gem's own functions, unchanged; call them on `NetCDF` (or bare, through the mixin):
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
value = NetCDF.nc_get_var1(ncid, vid, [0, 0]) # Numeric
|
|
117
|
+
NetCDF.nc_put_var1(ncid, vid, [0, 0], 273.15) # writes one cell
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The high-level API's scalar decode/encode wraps these calls in a 1-cell CArray to run through the codec — see [Codecs](04_codec.md).
|
|
121
|
+
|
|
122
|
+
## Definition, inquiry, attribute — all passthrough
|
|
123
|
+
|
|
124
|
+
The netcdf gem covers everything else. Call these on `NetCDF`, or bare through the mixin; only `nc_put_var` below is `CA::NetCDF`'s, because it writes a CArray:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# create + define
|
|
128
|
+
ncid = NetCDF.nc_create("out.nc", NetCDF::NC_CLOBBER | NetCDF::NC_NETCDF4)
|
|
129
|
+
tid = NetCDF.nc_def_dim(ncid, "time", 24)
|
|
130
|
+
vid = NetCDF.nc_def_var(ncid, "temp", NetCDF::NC_FLOAT, [tid])
|
|
131
|
+
NetCDF.nc_put_att(ncid, vid, "units", "K")
|
|
132
|
+
NetCDF.nc_enddef(ncid)
|
|
133
|
+
|
|
134
|
+
# write and close
|
|
135
|
+
NC.nc_put_var(ncid, vid, CArray.float32(24).seq!)
|
|
136
|
+
NetCDF.nc_close(ncid)
|
|
137
|
+
|
|
138
|
+
# inquire
|
|
139
|
+
ncid = NetCDF.nc_open("out.nc", NetCDF::NC_NOWRITE)
|
|
140
|
+
NetCDF.nc_inq_ndims(ncid) # => 1
|
|
141
|
+
NetCDF.nc_inq_nvars(ncid) # => 1
|
|
142
|
+
NetCDF.nc_inq_varname(ncid, 0) # => "temp"
|
|
143
|
+
NetCDF.nc_inq_vardimid(ncid, 0) # => [0]
|
|
144
|
+
NetCDF.nc_inq_dimlen(ncid, 0) # => 24
|
|
145
|
+
NetCDF.nc_close(ncid)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Moving between the two APIs
|
|
149
|
+
|
|
150
|
+
A `File`, `Group` or `Var` gives the ids the low-level API takes: `nc.ncid`, `group.ncid` and `var.varid`. A `File` keeps track of define and data mode itself, so inside one, switch modes with `nc.ensure_data_mode` / `nc.ensure_define_mode` rather than `nc_enddef` / `nc_redef`:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
NC::File.open("big.nc") do |nc|
|
|
154
|
+
buf = CArray.float32(73, 144)
|
|
155
|
+
NC.nc_get_vara(nc.ncid, nc["temp"].varid, [0, 0, 0], [1, 73, 144], buf)
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The high-level objects read a file's dimensions, variables, groups and attributes when it is opened, and keep them up to date through their own `def_*` calls. A change made through the low-level API — an attribute added with `nc_put_att`, a variable renamed or defined on `nc.ncid` — is seen after `nc.reload` (or `group.reload`), which also chooses the codec again; take new `Var` / `Dim` objects after it.
|
|
160
|
+
|
|
161
|
+
`CA::NetCDF` leaves the netcdf gem's `NetCDF` module unchanged: `NetCDF.nc_get_vara(ncid, varid, start, count, buf)` still reads into whatever buffer you pass.
|