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.
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
- The codec runs transparently inside `Var#[]` / `Var#[]=` (both array and scalar paths — see the [reading chapter](02_reading.md#hyperslab-reads-ncvari-j-k)). You usually don't call codec methods directly.
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.). Write: reverse. Runs FIRST so fill/valid/scale all see the correct numeric interpretation. |
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`. 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). |
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 last two rows are important: **this codec is a var-local value codec, not a CF data model builder**. Coordinate systems, cell methods, standard names, grid mappings — all preserved as attributes (`var.att("grid_mapping")` works), but the codec does not interpret them, nor does it walk to sibling variables. If you need CF data model support (a real coordinate-system layer akin to netCDF-Java's `CoordSystemBuilder`), either build it on top of this codec or reach for a heavier library.
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` inspects the `Conventions` global attribute:
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
- Naming rules (checked in registry order):
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
- Override on open:
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] # => UNDEF (scalar decode too)
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 without a `_FillValue` raises:
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 masked cells
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 (`1.0 / 0.1 == 9.999...` → `9`); the codec rounds. Symmetric on read: no rounding, just the linear transform.
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
- The codec stores CATime at the base resolution (day, in this case) and folds the count `5` into the numeric values. That way any reference date representable in the base works — including the normal case where the ref doesn't land on the coarser `count × base` grid from the epoch (e.g. `"5 days since 2024-06-15"` — 2024-06-15 is not on a 5-day-from-1970 grid, but it IS on the day grid). The count information isn't lost: it stays on `atts["units"]`, we just don't bake it into CATime's storage tick.
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
- # raw N reads as N*5 days past 2024-06-15
220
- dt = CA_INT64([0, 5, 10]).time(unit: :D, origin: "2024-06-15")
221
- # writing dt to units="5 days since 2024-06-15":
222
- # codec.timesteps(unit: :D, origin: "2024-06-15") -> [0, 5, 10]
223
- # then / 5 -> [0, 1, 2] (on-disk)
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`, or missing → CATime (proleptic Gregorian, UTC).
244
- - Anything else — `noleap`, `365_day`, `all_leap`, `366_day`, `360_day`, `julian`, `none` — is unsupported. The codec warns once per distinct calendar value and returns raw values unchanged. Writing a `CATime` to a variable with an unsupported calendar raises.
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
- For post-1582 dates, `standard` and `proleptic_gregorian` agree, so CATime is exact for modern climate/ocean data. Pre-1582 or non-standard calendars belong in an external gem.
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 — raw bytes only:
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 round-tripping unknown files, debugging what a producer actually wrote, or side-stepping a codec bug.
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. Every adherent COARDS file is also an adherent CF file — spec-inclusion, not just similarity — so `COARDS < CF` inherits every transform; only `self.claims?` differs. Everything above (`_FillValue`, scale/offset, `valid_range`, CF time, calendar policy) applies verbatim.
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
- **Check `is_a?(COARDS)` BEFORE `is_a?(CF)`** in an upper-layer dispatcher — COARDS pins the coordinate axis order to `T, Z, Y, X`, so an upper layer that treats a COARDS file as generic CF may silently accept looser axis orderings. (This value codec doesn't touch axis order, so at THIS layer the distinction doesn't matter; it matters at the layer above.)
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"` alone.
278
- - Combos starting with `COARDS` (e.g. `"COARDS/CF-1.0"`) — some tools emit these.
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: class method for auto-selection by the Conventions attribute.
289
- # Only inspects the Conventions string — deliberately does NOT take the
290
- # File, to keep dispatch out of the CDM Layer 2 territory (a "look at
291
- # coordinate variables to sniff CF" hook is intentionally not provided).
292
- def self.claims?(conventions_string) = ...
293
-
294
- # Required: the two transforms. Both are var-local — see the contract
295
- # note earlier in this chapter.
296
- def decode(var, ca) = ... # var: Var, ca: raw CArray -> processed value
297
- def encode(var, ca) = ... # var: Var, ca: value -> raw CArray for the on-disk write
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
- Registered codecs are matched in reverse registration order (later registrations win). `resolve` accepts:
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
- ## Full pipeline order
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
- 1. `encode_time` — `CATime` → `count-since-ref`.
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
- (`valid_range` has no defined encode direction in CF, and the codec doesn't invent one.)
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
- ## Debug / inspect a variable's decode
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 # bypass codec → the on-disk values
359
- decoded = v.to_ca # === v.codec.decode(v, v.raw) (identity)
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
- The identity `v.to_ca == v.codec.decode(v, v.raw)` holds by construction, so `v.raw` is the canonical descent point when you want to work below the codec — no `instance_variable_get` needed.
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` 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.
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 # => "/" (netcdf's root 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
- Every group has its own global-attribute set (netcdf-4 changes `NC_GLOBAL` from a single set to per-group). `def_att` / `atts` / `att` on a Group operate on that set:
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` | raw group ncid (int) |
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.