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