carray-netcdf 1.0.13 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,41 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "carray-netcdf"
3
+ spec.version = File.read(File.expand_path("lib/carray/netcdf/version.rb", __dir__))[/VERSION\s*=\s*"([^"]+)"/, 1]
4
+ spec.summary = "A NetCDF interface for CArray"
5
+ spec.description = <<~TEXT
6
+ A NetCDF interface for CArray, built on the netcdf gem. Data is
7
+ exchanged as CArray at every level, through Ruby's MemoryView protocol,
8
+ so libnetcdf works on the array's own memory.
1
9
 
2
- Gem::Specification::new do |s|
3
- version = "1.0.13"
10
+ Two layers do this. Layer 1 turns the netcdf gem's MemoryView buffers
11
+ into CArray, keeping the nc_* functions as they are. Layer 2 builds the
12
+ object-oriented interface on top, and returns data as the file's
13
+ conventions define it: _FillValue as a mask, scale_factor and add_offset
14
+ applied, CF time as CATime. Pure Ruby, on CArray 3.0.
15
+ TEXT
16
+ spec.authors = ["himotoyoshi"]
17
+ spec.email = ["himotoyoshi@users.noreply.github.com"]
18
+ spec.license = "MIT"
19
+ spec.homepage = "https://github.com/himotoyoshi/carray-netcdf"
4
20
 
5
- files = Dir.glob("**/*") - [
6
- Dir.glob("carray-*.gem"),
7
- ].flatten
21
+ spec.required_ruby_version = ">= 3.1.0"
8
22
 
9
- s.platform = Gem::Platform::RUBY
10
- s.name = "carray-netcdf"
11
- s.summary = "Extension for manipulating NetCDF3 file with CArray"
12
- s.description = <<-HERE
13
- Extension for manipulating NetCDF3 file with CArray
14
- HERE
15
- s.version = version
16
- s.author = "Hiroki Motoyoshi"
17
- s.email = ""
18
- s.homepage = 'https://github.com/himotoyoshi/carray-netcdf'
19
- s.files = files
20
- s.extensions = [ "ext/extconf.rb" ]
21
- s.executables << 'ncdef2yaml'
22
- s.required_ruby_version = ">= 1.8.1"
23
+ spec.files = Dir[
24
+ "lib/**/*.rb",
25
+ "bin/*",
26
+ "README.md",
27
+ "CONTRIBUTING.md",
28
+ "CHANGELOG.md",
29
+ "docs/*.md",
30
+ "LICENSE",
31
+ "carray-netcdf.gemspec",
32
+ ]
33
+ spec.require_paths = ["lib"]
34
+ spec.bindir = "bin"
35
+ spec.executables = %w[ncheader ncvar]
36
+
37
+ # A CArray is handed to libnetcdf as memory, so the ceiling is the next
38
+ # minor on both sides.
39
+ spec.add_dependency "carray", ">= 3.0.1"
40
+ spec.add_dependency "netcdf", ">= 0.1.0", "< 0.2"
23
41
  end
@@ -0,0 +1,72 @@
1
+ # Introduction
2
+
3
+ carray-netcdf is a NetCDF interface for CArray, built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem. This guide takes NetCDF and CArray as known, and covers what this gem adds between them.
4
+
5
+ ## What is carray-netcdf
6
+
7
+ A NetCDF file holds arrays, and in Ruby the array is CArray. The purpose of this gem is to connect the two directly: open a file, get CArrays out of it, compute, and put CArrays back into a file that other NetCDF tools can read.
8
+
9
+ Two things stand between a file and a CArray you can compute on. The first is the values themselves. What a file stores is often not what you want to work with — a physical field packed into `short` with a `scale_factor`, missing values marked by a `_FillValue` sentinel, a time axis kept as numbers since an epoch. Reading those correctly means following the conventions the file declares, and the gem does it for you, so what comes back is a masked CArray of physical values and a CATime axis.
10
+
11
+ The second is size. Files are routinely larger than memory, so reading a variable has to mean reading a part of it. Here a variable is indexed the way a CArray is, and only the part you index is read.
12
+
13
+ ## Two layers, one module
14
+
15
+ The netcdf gem is a literal translation of libnetcdf, and it passes data as MemoryView buffers. Two layers turn that into CArray, and both live in the module `CA::NetCDF`:
16
+
17
+ ```
18
+ netcdf (dependency) thin libnetcdf binding, MemoryView buffers
19
+ ↑
20
+ CA::NetCDF
21
+ ├─ layer 1 the nc_* functions, with CArray in and out
22
+ └─ layer 2 File / Group / Var / Dim, and the codecs
23
+ ```
24
+
25
+ Layer 1 keeps the `nc_*` functions as they are and only changes what they take and return: a `get` hands back a CArray, a `put` accepts one. Use it as module functions, or mix it into a class of your own when you want the C idiom with CArray at both ends.
26
+
27
+ Layer 2 is built on layer 1 and is the everyday surface: a file is an object, a variable is indexed like a CArray, and a codec decodes values on the way out.
28
+
29
+ ## Convention in this guide
30
+
31
+ Every example assumes these two lines:
32
+
33
+ ```ruby
34
+ require "carray-netcdf"
35
+ NC = CA::NetCDF
36
+ ```
37
+
38
+ Requiring the gem loads CArray, the netcdf gem, and both layers.
39
+
40
+ ## What the gem covers
41
+
42
+ Whole NetCDF files, read and written: dimensions including unlimited ones, variables of every type the netcdf gem binds, attributes on a variable or on the file, and NetCDF-4 groups. Values pass through a codec in both directions, so `_FillValue`, `scale_factor`, `add_offset` and CF time are handled on the way out and on the way in. A file can be created atomically, or with its schema copied from an existing file. A Zarr store is read and written through the same calls, given a URL in place of a path.
43
+
44
+ ## Threads and Ractors
45
+
46
+ Array I/O releases the GVL while libnetcdf works, so a large read or write does not hold up other Ruby threads. Do not touch a CArray from another thread while it is being read into or written from.
47
+
48
+ libnetcdf itself is not thread-safe, so the netcdf gem lets one call at a time into it, across the whole process. Threads therefore do not make netCDF I/O faster, only the Ruby work around it. That lock covers single calls: a `File`, `Group` or `Var` keeps state of its own between calls, so share one between threads only under a lock of your own.
49
+
50
+ NetCDF works only from the main Ractor. Opening a file in any other Ractor raises. For netCDF I/O in parallel, use separate processes.
51
+
52
+ ## Beyond this gem
53
+
54
+ Three things a CF file may carry are not turned into anything by this gem, and are left to the layer above it or to your own code.
55
+
56
+ - **The CF data model.** Coordinate systems, grid mappings, cell methods, standard names and ancillary variables are kept as plain attributes — `var.att("grid_mapping")` still works — but no coordinate objects are built from them. That belongs to a heavier layer, or to your own code.
57
+ - **Non-standard calendars** for CF time (`noleap`, `360_day`, `julian` and the rest). CATime is proleptic Gregorian, in UTC, so a file declaring another calendar warns once and its time coordinate comes back as the raw numbers.
58
+ - **Time finer than CATime's base tick.** A variable in `"hours since 2022-01-01"` decodes to whole hours. Declare a finer unit, such as `"seconds since ..."`, if you need sub-hour precision.
59
+
60
+ The [codec chapter](04_codec.md) states in full which CF attributes are honoured and which are passed through untouched.
61
+
62
+ ## How this guide is arranged
63
+
64
+ - [Getting started](01_getting_started.md) — install, then read and write a first file
65
+ - [Reading files](02_reading.md) — opening a file, finding a variable, reading all or part of it
66
+ - [Writing files](03_writing.md) — declaring the schema, writing data, changing a file later
67
+ - [Codecs and CF conventions](04_codec.md) — what gets decoded, how a codec is chosen, writing your own
68
+ - [Groups](05_groups.md) — NetCDF-4 groups, and what they inherit
69
+ - [Layer 1](06_layer1.md) — the `nc_*` functions with CArray in and out
70
+ - [Cookbook](07_cookbook.md) — worked solutions to real tasks
71
+ - [Cheatsheet](08_cheatsheet.md) — every method and constant of the gem on one page
72
+ - [Command-line tools](09_cli.md) — `ncheader` and `ncvar`, which ship with the gem
@@ -0,0 +1,114 @@
1
+ # Getting started
2
+
3
+ ## Install
4
+
5
+ libnetcdf and its headers have to be present first, because the [netcdf](https://github.com/himotoyoshi/netcdf) gem underneath is built against them:
6
+
7
+ ```
8
+ brew install netcdf # macOS
9
+ apt install libnetcdf-dev # Debian / Ubuntu
10
+ dnf install netcdf-devel # Fedora / RHEL
11
+ ```
12
+
13
+ Then:
14
+
15
+ ```
16
+ gem install carray-netcdf
17
+ ```
18
+
19
+ That brings in CArray and the netcdf gem.
20
+
21
+ Requires:
22
+
23
+ - Ruby >= 3.1
24
+ - carray >= 3.0.1, < 3.1
25
+ - libnetcdf >= 4.1
26
+
27
+ ## Loading
28
+
29
+ ```ruby
30
+ require "carray-netcdf"
31
+
32
+ NC = CA::NetCDF # optional short alias used throughout this guide
33
+ ```
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
+ ## Reading in three lines
38
+
39
+ ```ruby
40
+ NC::File.open("data.nc") do |nc|
41
+ data = nc["temperature"].to_ca # => CArray (already decoded via CF codec if applicable)
42
+ end
43
+ ```
44
+
45
+ - `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.
48
+
49
+ ## Reading a slice
50
+
51
+ ```ruby
52
+ NC::File.open("data.nc") do |nc|
53
+ t = nc["temperature"]
54
+ t.shape # => e.g. [365, 73, 144]
55
+ t.dims.map(&:name) # => ["time", "lat", "lon"]
56
+
57
+ first_day = t[0, nil, nil] # 2-D CArray, shape [73, 144]
58
+ point = t[0, 0, 0] # scalar (Float, or CATime::Element for a time coord)
59
+ block = t[0..9, nil, nil] # first 10 days, 3-D
60
+ end
61
+ ```
62
+
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.
64
+
65
+ ## Writing a file in ten lines
66
+
67
+ ```ruby
68
+ NC::File.create("out.nc") do |nc|
69
+ nc.def_dim("time", :unlimited)
70
+ nc.def_dim("lat", 73)
71
+ nc.def_dim("lon", 144)
72
+
73
+ nc.def_var("temp", :float, ["time", "lat", "lon"],
74
+ units: "K", _FillValue: -999.9)
75
+
76
+ nc.def_att("Conventions", "CF-1.7")
77
+ nc.def_att("title", "Demo file")
78
+
79
+ nc["temp"] = CArray.float32(1, 73, 144) { rand * 30 + 260 }
80
+ end
81
+ ```
82
+
83
+ - `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.
86
+ - `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
+
88
+ ## The three access idioms
89
+
90
+ Everything below reads the same underlying data:
91
+
92
+ ```ruby
93
+ NC::File.open("data.nc") do |nc|
94
+ # (1) full read, then work with a CArray
95
+ ca = nc["temp"].to_ca
96
+ ca.mean(0)
97
+
98
+ # (2) hyperslab read via CArray indexing on the Var
99
+ slab = nc["temp"][0..9, nil, nil]
100
+
101
+ # (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.
104
+ nc["temp"].mean
105
+ end
106
+ ```
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.
@@ -0,0 +1,216 @@
1
+ # Reading files
2
+
3
+ A read goes through three steps, and only the last one touches the disk:
4
+
5
+ ```
6
+ nc = NC::File.open(path) # a file handle: its dimensions, variables and attributes
7
+ var = nc["temp"] # a variable handle — still no data read
8
+ ca = var[0, nil, nil] # this is the read: libnetcdf fills a CArray, the codec decodes it
9
+ ```
10
+
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
+
13
+ Everything else on this page is detail around those three steps.
14
+
15
+ ## Opening
16
+
17
+ ```ruby
18
+ require "carray-netcdf"
19
+ NC = CA::NetCDF
20
+
21
+ # Block form (recommended — auto-closes on exit / exception)
22
+ NC::File.open("data.nc") do |nc|
23
+ # ...
24
+ end
25
+
26
+ # Explicit form
27
+ nc = NC::File.open("data.nc")
28
+ begin
29
+ # ...
30
+ ensure
31
+ nc.close
32
+ end
33
+ ```
34
+
35
+ `open` takes an optional mode:
36
+
37
+ ```ruby
38
+ NC::File.open("data.nc") # :read (default)
39
+ NC::File.open("data.nc", :write) # open for modification (existing file)
40
+ NC::File.open("data.nc", :append) # alias of :write
41
+ ```
42
+
43
+ For creating a new file, see [Writing files](03_writing.md).
44
+
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).
46
+
47
+ ## Introspection
48
+
49
+ ```ruby
50
+ NC::File.open("data.nc") do |nc|
51
+ 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)
54
+
55
+ nc.dims # => {"time" => Dim, "lat" => Dim, "lon" => Dim}
56
+ nc.vars # => {"temp" => Var, "time" => Var, "lat" => Var, "lon" => Var}
57
+ nc.atts # => {"Conventions" => "CF-1.7", "title" => "…", ...} (frozen)
58
+
59
+ nc.dim("time") # => Dim
60
+ nc.var("temp") # => Var
61
+ nc.att("title") # => "..." (or nil)
62
+
63
+ nc.dim?("time") # => true
64
+ nc.var?("bogus") # => false
65
+ end
66
+ ```
67
+
68
+ Names are Strings — always. `nc["temp"]` and `nc.var("temp")` work; Symbols are not coerced.
69
+
70
+ ## The Var handle
71
+
72
+ `nc["temp"]` returns a `Var`. It is a **lazy handle**: no I/O happens until you index into it or call `to_ca`.
73
+
74
+ ```ruby
75
+ t = nc["temp"]
76
+
77
+ t.name # => "temp"
78
+ t.shape # => [365, 73, 144]
79
+ t.ndim # => 3
80
+ t.data_type # => :float32 (Symbol, matching CArray convention)
81
+ t.nc_type # => NetCDF::NC_FLOAT (the raw nc_type integer)
82
+ t.dims # => [Dim("time"), Dim("lat"), Dim("lon")]
83
+ t.atts # => {"units" => "K", "_FillValue" => -999.9, ...} (frozen)
84
+ t.att("units") # => "K"
85
+
86
+ t.file # => the enclosing File
87
+ t.group # => the enclosing Group (== File for a root-level variable)
88
+ t.codec # => the codec that will decode reads from this variable
89
+ ```
90
+
91
+ ## Whole-variable read
92
+
93
+ ```ruby
94
+ ca = nc["temp"].to_ca # equivalent to nc["temp"][]
95
+ ```
96
+
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.
98
+
99
+ ## Hyperslab reads (`nc[var][i, j, k]`)
100
+
101
+ `Var#[]` accepts anything `CArray.scan_index` accepts:
102
+
103
+ ```ruby
104
+ t = nc["temp"]
105
+
106
+ t[] # whole variable (same as t.to_ca)
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
110
+ t[0, 0, 0] # single element (scalar — Float / Integer / Element)
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)
114
+ ```
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.
119
+
120
+ Scalar decoding examples:
121
+
122
+ ```ruby
123
+ nc["temp"][0, 0, 0] # => 273.15 (Float, after scale/offset)
124
+ nc["temp"][0, 0, 0] # => UNDEF (if that cell held _FillValue)
125
+ nc["time"][0] # => #<CATime::Element 2022-01-01T00:00:00Z>
126
+ nc["time"][0].to_time # => 2022-01-01 00:00:00 UTC (Ruby Time)
127
+ nc["time"][0].to_date # => #<Date: 2022-01-01>
128
+ ```
129
+
130
+ ## `method_missing`: use a Var like a CArray
131
+
132
+ `Var` forwards unknown methods to `to_ca`, so anything you can call on a CArray, you can call on a Var directly:
133
+
134
+ ```ruby
135
+ nc["temp"].mean # => the mean of the whole array
136
+ nc["temp"].max # => the max
137
+ nc["temp"].shape # (Var#shape is defined, so this is not method_missing)
138
+ nc["temp"].sum(0) # => reduction along axis 0
139
+ ```
140
+
141
+ A method that changes its receiver — `fill`, `seq!`, `mask=`, anything ending in `!` or `=` — raises `NoMethodError` instead: it would change the fresh array `to_ca` returns, not the file. Write with `v[...] = ...`.
142
+
143
+ Each such call re-reads the whole variable. For repeated work, cache once:
144
+
145
+ ```ruby
146
+ temp = nc["temp"].to_ca
147
+ temp.mean
148
+ temp.max
149
+ temp.sum(0)
150
+ ```
151
+
152
+ ## Reading a subset into an existing buffer
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:
155
+
156
+ ```ruby
157
+ NC::File.open("big.nc") do |nc|
158
+ varid = ::NetCDF.nc_inq_varid(nc.ncid, "temp")
159
+ buf = CArray.float32(73, 144) # one time slice, reused
160
+
161
+ 365.times do |k|
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.
164
+ process(buf)
165
+ end
166
+ end
167
+ ```
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.
170
+
171
+ ## Dimensions
172
+
173
+ ```ruby
174
+ d = nc.dim("time")
175
+
176
+ d.name # => "time"
177
+ d.length # => 365
178
+ d.unlimited? # => true / false
179
+ d.to_i # => raw dimid (rarely needed)
180
+
181
+ # Bridge to the coordinate variable (a variable of the same name)
182
+ d.var # => Var, or nil if no matching variable exists
183
+ d.to_ca # => the coord values (nil if no coord variable)
184
+ ```
185
+
186
+ ## Attributes
187
+
188
+ Attributes are surfaced as native Ruby values by the underlying netcdf gem:
189
+
190
+ - `NC_CHAR` / `NC_STRING` → `String`
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)
193
+
194
+ Examples:
195
+
196
+ ```ruby
197
+ nc.att("Conventions") # => "CF-1.7"
198
+ nc.att("title") # => "Demo file"
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])
202
+ ```
203
+
204
+ An unknown attribute returns `nil`. `nc.atts` returns the frozen hash of everything (global on `nc`, per-variable on `nc["temp"]`).
205
+
206
+ ## Turning decoding off
207
+
208
+ If you want raw bytes — no `_FillValue` masking, no unpacking, no CATime — pass `codec: :identity`:
209
+
210
+ ```ruby
211
+ NC::File.open("data.nc", codec: :identity) do |nc|
212
+ raw = nc["temp"].to_ca # packed short values, fill sentinel as-is, ...
213
+ end
214
+ ```
215
+
216
+ Codec selection is covered in detail in the [Codecs chapter](04_codec.md).