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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/CONTRIBUTING.md +75 -0
- data/LICENSE +21 -0
- data/README.md +108 -2
- data/bin/ncheader +223 -0
- data/bin/ncvar +160 -0
- data/carray-netcdf.gemspec +37 -19
- data/docs/00_introduction.md +72 -0
- data/docs/01_getting_started.md +114 -0
- data/docs/02_reading.md +216 -0
- data/docs/03_writing.md +418 -0
- data/docs/04_codec.md +373 -0
- data/docs/05_groups.md +200 -0
- data/docs/06_layer1.md +160 -0
- data/docs/07_cookbook.md +345 -0
- data/docs/08_cheatsheet.md +326 -0
- data/docs/09_cli.md +79 -0
- data/lib/carray/netcdf/codec.rb +536 -0
- data/lib/carray/netcdf/dim.rb +67 -0
- data/lib/carray/netcdf/file.rb +354 -0
- data/lib/carray/netcdf/group.rb +517 -0
- data/lib/carray/netcdf/var.rb +539 -0
- data/lib/carray/netcdf/version.rb +5 -0
- data/lib/carray/netcdf.rb +287 -0
- data/lib/carray-netcdf.rb +3 -1
- metadata +80 -25
- data/API.md +0 -270
- data/Rakefile +0 -12
- data/bin/ncdef2yaml +0 -60
- data/examples/test.nc +0 -0
- data/examples/test.rb +0 -41
- data/examples/time.rb +0 -11
- data/ext/extconf.rb +0 -11
- data/ext/rb_netcdflib.c +0 -2674
- data/lib/io/netcdf.rb +0 -848
data/docs/06_layer1.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Layer 1 — raw `nc_*` API
|
|
2
|
+
|
|
3
|
+
Everyday code uses layer 2 (`File`, `Var`, ...). Layer 1 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 open by ncid or work outside the `File` object model.
|
|
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 every layer-2 semantics for a specific operation.
|
|
11
|
+
|
|
12
|
+
## The module
|
|
13
|
+
|
|
14
|
+
`CA::NetCDF` itself IS layer 1. It `include`s `::NetCDF` (the netcdf gem's module) so all `nc_*` functions and `NC_*` constants pass through, and overrides only the array I/O to speak CArray.
|
|
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 layer 1 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 this layer includes without modifying.
|
|
44
|
+
|
|
45
|
+
## `nc_get_var*` — buffer-less returns a CArray
|
|
46
|
+
|
|
47
|
+
Without a buffer argument, layer 1 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("data.nc", ::NetCDF::NC_NOWRITE)
|
|
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 layer 2 for those.
|
|
62
|
+
|
|
63
|
+
A type with no CArray counterpart — opaque, `NC_STRING` — raises `NotImplementedError`; read an opaque variable into a buffer of your own.
|
|
64
|
+
|
|
65
|
+
## `nc_get_var*` — with a buffer, reads into it
|
|
66
|
+
|
|
67
|
+
Pass an existing CArray and layer 1 asks netcdf to read into that buffer. Perfect for streaming a slab at a time with **zero 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 C-contiguous (a plain entity CArray is; a view is not — the gem will refuse a non-contiguous buffer). 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 CABlock view; layer 1 does .copy on your behalf
|
|
99
|
+
# and passes the resulting entity to nc_put_vara.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A **masked** CArray is *refused*. Layer 1 does no fill encoding — that's layer 2's job (via the codec). 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 pure passthrough (the netcdf gem handles them directly), included in layer 1 so the mixin style has them:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
value = NC.nc_get_var1(ncid, vid, [0, 0]) # Numeric
|
|
117
|
+
NC.nc_put_var1(ncid, vid, [0, 0], 273.15) # writes one cell
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Layer 2'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. In layer-1 code, use them bare (via mixin) or fully qualified:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# create + define
|
|
128
|
+
ncid = NC.nc_create("out.nc", NC::NC_CLOBBER | NC::NC_NETCDF4)
|
|
129
|
+
tid = NC.nc_def_dim(ncid, "time", 24)
|
|
130
|
+
vid = NC.nc_def_var(ncid, "temp", NC::NC_FLOAT, [tid])
|
|
131
|
+
NC.nc_put_att(ncid, vid, "units", "K")
|
|
132
|
+
NC.nc_enddef(ncid)
|
|
133
|
+
|
|
134
|
+
# write and close
|
|
135
|
+
NC.nc_put_var(ncid, vid, CArray.float32(24).seq!)
|
|
136
|
+
NC.nc_close(ncid)
|
|
137
|
+
|
|
138
|
+
# inquire
|
|
139
|
+
ncid = NC.nc_open("out.nc", NC::NC_NOWRITE)
|
|
140
|
+
NC.nc_inq_ndims(ncid) # => 1
|
|
141
|
+
NC.nc_inq_nvars(ncid) # => 1
|
|
142
|
+
NC.nc_inq_varname(ncid, 0) # => "temp"
|
|
143
|
+
NC.nc_inq_vardimid(ncid, 0) # => [0]
|
|
144
|
+
NC.nc_inq_dimlen(ncid, 0) # => 24
|
|
145
|
+
NC.nc_close(ncid)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Any of these can also be reached as `::NetCDF.nc_*` (unchanged), or through layer 2's higher-level surface.
|
|
149
|
+
|
|
150
|
+
## Coexisting with `::NetCDF` and future `Numo::NetCDF`
|
|
151
|
+
|
|
152
|
+
Layer 1 does **not** modify `::NetCDF`. It only adds behavior in the `CA::NetCDF` namespace. So an app that mixes carray-netcdf with a hypothetical `Numo::NetCDF` gem (which would follow the same pattern in its own namespace) can call:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
CA::NetCDF.nc_get_vara(ncid, vid, s, c) # -> CArray
|
|
156
|
+
Numo::NetCDF.nc_get_vara(ncid, vid, s, c) # -> Numo::NArray (hypothetical)
|
|
157
|
+
::NetCDF.nc_get_vara(ncid, vid, s, c, buf) # -> buf, any MemoryView producer (untouched)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
with no clashes. This coexistence is exactly why layer 1 avoids monkey-patching `::NetCDF`.
|
data/docs/07_cookbook.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Cookbook
|
|
2
|
+
|
|
3
|
+
End-to-end scenarios. Each example is self-contained; copy, adapt, run.
|
|
4
|
+
|
|
5
|
+
## 1. Write a CF-1.7 hourly temperature file
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require "carray-netcdf"
|
|
9
|
+
NC = CA::NetCDF
|
|
10
|
+
|
|
11
|
+
NC::File.create("hourly_temp.nc") do |nc|
|
|
12
|
+
# Dimensions
|
|
13
|
+
nc.def_dim("time", :unlimited)
|
|
14
|
+
nc.def_dim("lat", 73)
|
|
15
|
+
nc.def_dim("lon", 144)
|
|
16
|
+
|
|
17
|
+
# Coordinate variables
|
|
18
|
+
nc.def_var("time", :int, ["time"],
|
|
19
|
+
standard_name: "time",
|
|
20
|
+
units: "hours since 2022-01-01T00:00:00Z",
|
|
21
|
+
calendar: "standard")
|
|
22
|
+
nc.def_var("lat", :float, ["lat"],
|
|
23
|
+
standard_name: "latitude", units: "degrees_north")
|
|
24
|
+
nc.def_var("lon", :float, ["lon"],
|
|
25
|
+
standard_name: "longitude", units: "degrees_east")
|
|
26
|
+
|
|
27
|
+
# Data variable
|
|
28
|
+
nc.def_var("temp", :float, ["time", "lat", "lon"],
|
|
29
|
+
standard_name: "air_temperature",
|
|
30
|
+
units: "K",
|
|
31
|
+
_FillValue: -999.9)
|
|
32
|
+
|
|
33
|
+
# Globals
|
|
34
|
+
nc.def_att("Conventions", "CF-1.7")
|
|
35
|
+
nc.def_att("title", "Synthetic hourly temperature")
|
|
36
|
+
|
|
37
|
+
# Coord data
|
|
38
|
+
nc["lat"] = CArray.float32(73) { |i| -90.0 + i * 2.5 }
|
|
39
|
+
nc["lon"] = CArray.float32(144) { |i| i * 2.5 }
|
|
40
|
+
nc["time"] = CArray.time_series("2022-01-01T00:00:00Z", count: 24, unit: :h)
|
|
41
|
+
|
|
42
|
+
# Data (24 hourly slabs)
|
|
43
|
+
24.times do |t|
|
|
44
|
+
nc["temp", t, nil, nil] = CArray.float32(73, 144) { |i, j|
|
|
45
|
+
273.15 + 15.0 * Math.cos(i * 0.05) + 5.0 * Math.sin(j * 0.05 + t * 0.1)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Read it back:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
NC::File.open("hourly_temp.nc") do |nc|
|
|
55
|
+
nc.att("Conventions") # => "CF-1.7"
|
|
56
|
+
|
|
57
|
+
t = nc["time"].to_ca
|
|
58
|
+
t.class # => CATime
|
|
59
|
+
t.first.to_s # => "2022-01-01T00:00:00Z"
|
|
60
|
+
t.last.to_s # => "2022-01-01T23:00:00Z"
|
|
61
|
+
|
|
62
|
+
temp = nc["temp"].to_ca # decoded (fill -> mask if present)
|
|
63
|
+
temp.shape # => [24, 73, 144]
|
|
64
|
+
temp.mean # → around 273-285 K
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 2. Read the third time step of a CF file, mask-aware
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
NC::File.open("hourly_temp.nc") do |nc|
|
|
72
|
+
slab = nc["temp"][2, nil, nil] # rank-2 CArray, shape [73, 144]
|
|
73
|
+
slab.count_masked # 0 unless the file has fill cells here
|
|
74
|
+
slab_at_equator = slab[36, nil] # rank-1 CArray, shape [144]
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 3. Pack a physical field into a short (satellite-style storage)
|
|
79
|
+
|
|
80
|
+
Storing a physical range that fits comfortably in `short` saves half the space of `float`. CF's `scale_factor` / `add_offset` handle it:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
NC::File.create("packed.nc") do |nc|
|
|
84
|
+
nc.def_dim("n", 4)
|
|
85
|
+
nc.def_var("temp", :short, ["n"],
|
|
86
|
+
units: "K",
|
|
87
|
+
scale_factor: 0.01, # 0.01 K resolution
|
|
88
|
+
add_offset: 250.0, # centered around 250 K
|
|
89
|
+
_FillValue: -32768,
|
|
90
|
+
valid_range: [-10000, 10000]) # ±100 K around offset, in packed space
|
|
91
|
+
nc.def_att("Conventions", "CF-1.7")
|
|
92
|
+
|
|
93
|
+
# Write physical values; the codec packs them into short automatically.
|
|
94
|
+
nc["temp"] = CA_DOUBLE([250.00, 275.00, 300.00, 325.00])
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Round-trip verification:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Physical view (default codec = CF)
|
|
102
|
+
NC::File.open("packed.nc") do |nc|
|
|
103
|
+
nc["temp"].to_ca.to_a # => [250.00, 275.00, 300.00, 325.00]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Raw view (Identity codec)
|
|
107
|
+
NC::File.open("packed.nc", codec: :identity) do |nc|
|
|
108
|
+
nc["temp"].to_ca.to_a # => [0, 2500, 5000, 7500]
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Optionally enforce `valid_range` masking:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
NC::File.open("packed.nc", codec: [:cf, mask_valid_range: true]) do |nc|
|
|
116
|
+
nc["temp"].to_ca # cells outside [-10000, 10000] packed -> UNDEF
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 4. Stream a large variable one slab at a time
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
require "carray-netcdf"
|
|
124
|
+
NC = CA::NetCDF
|
|
125
|
+
|
|
126
|
+
# Assume /big.nc has variable temp[time, lat, lon] with time = 10000.
|
|
127
|
+
# Compute the daily mean without ever holding the whole array.
|
|
128
|
+
|
|
129
|
+
means = CArray.float64(10000)
|
|
130
|
+
|
|
131
|
+
NC::File.open("/big.nc") do |nc|
|
|
132
|
+
temp = nc["temp"]
|
|
133
|
+
n_time = temp.shape[0]
|
|
134
|
+
|
|
135
|
+
# For zero-allocation streaming, drop to layer 1. Layer 2's hyperslab
|
|
136
|
+
# read allocates a fresh CArray per step; layer 1 lets us reuse a buffer.
|
|
137
|
+
vid = ::NetCDF.nc_inq_varid(nc.ncid, "temp")
|
|
138
|
+
buf = CArray.float32(1, *temp.shape[1..]) # [1, lat, lon]
|
|
139
|
+
|
|
140
|
+
n_time.times do |k|
|
|
141
|
+
NC.nc_get_vara(nc.ncid, vid, [k, 0, 0], [1, *temp.shape[1..]], buf)
|
|
142
|
+
means[k] = buf.mean # raw physical bytes
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If your file has `_FillValue` or `scale_factor`, layer 1 doesn't apply them — you'd need to either drop that particular streaming loop into layer 2 (which allocates per slab but is one line: `means[k] = nc["temp"][k, nil, nil].mean`), or apply the transforms yourself.
|
|
148
|
+
|
|
149
|
+
## 4b. Generate a batch of files from a single reference
|
|
150
|
+
|
|
151
|
+
The classic "read one, write many" pipeline: hold a reference NetCDF open, loop over some units of work, and emit one output per iteration that inherits the reference's skeleton (dims, global atts, coordinate vars) and only diverges in the payload. `File.create(template:, vars:)` collapses the boilerplate; `clobber: :skip` + `atomic: true` make the loop restartable.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
NC::File.open("reference.nc") do |ref|
|
|
155
|
+
time_windows.each do |window| # e.g. one 24h chunk per iteration
|
|
156
|
+
NC::File.create("out_#{window.tag}.nc",
|
|
157
|
+
template: ref, # dims + atts + format inherited
|
|
158
|
+
vars: %w[lat lon], # coord vars: full bit-identical copy
|
|
159
|
+
clobber: :skip, # already-produced files are left alone
|
|
160
|
+
atomic: true) do |dst|
|
|
161
|
+
dst.def_var("time", :int, ["time"],
|
|
162
|
+
units: "hours since #{window.origin}", calendar: "standard")
|
|
163
|
+
dst.def_var("temp", :float, ["time","lat","lon"], _FillValue: -999.9)
|
|
164
|
+
dst["time"] = window.timestamps
|
|
165
|
+
window.each_slab { |t, slab| dst["temp", t, nil, nil] = slab }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Why the combination matters:
|
|
172
|
+
|
|
173
|
+
- **`template: ref`** — dims, `Conventions`, `title`, and any custom global attributes carry over. Output inherits the NetCDF-4 format if the reference is netcdf-4 (so groups / deflate work), unless `netcdf4:` is set explicitly.
|
|
174
|
+
- **`vars: %w[lat lon]`** — coordinate variables that don't change between outputs come along for the ride, byte-identical (no codec on either side). Downstream diff / dedup tools won't see spurious floating-point drift.
|
|
175
|
+
- **`clobber: :skip`** — reruns pick up where a previous run stopped; finished files are not touched. Combines cleanly with `atomic:`.
|
|
176
|
+
- **`atomic: true`** — a mid-loop crash leaves neither a half-written output nor a stale one. `Dir["out_*.nc"]` reflects exactly what completed successfully.
|
|
177
|
+
|
|
178
|
+
For read-side workflows that consume the output, the split is clean: either the file is fully there (produced by a completed `create` block), or it is not there yet. No corrupt-file recovery logic in downstream code.
|
|
179
|
+
|
|
180
|
+
## 5. Append time steps to an unlimited-dimension file
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# Existing file created with time = :unlimited.
|
|
184
|
+
NC::File.open("hourly_temp.nc", :write) do |nc|
|
|
185
|
+
cur = nc.dim("time").length # e.g. 24
|
|
186
|
+
|
|
187
|
+
new_slice = CArray.float32(73, 144) { |i, j| 273.15 + Math.sin(i*0.1) }
|
|
188
|
+
nc["temp", cur, nil, nil] = new_slice # writes at t=cur; time grows to cur+1
|
|
189
|
+
|
|
190
|
+
nc["time", cur] = CArray.time("2022-01-02T00:00:00Z", unit: :h)[0]
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 6. Group your model by subsystem (NetCDF-4)
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
NC::File.create("model.nc", netcdf4: true) do |nc|
|
|
198
|
+
nc.def_dim("time", :unlimited)
|
|
199
|
+
nc.def_att("Conventions", "CF-1.7")
|
|
200
|
+
|
|
201
|
+
ocean = nc.def_group("ocean")
|
|
202
|
+
ocean.def_dim("depth", 50)
|
|
203
|
+
ocean.def_var("temperature", :float, ["time", "depth"], units: "degC")
|
|
204
|
+
ocean.def_var("salinity", :float, ["time", "depth"], units: "PSU")
|
|
205
|
+
|
|
206
|
+
atm = nc.def_group("atmosphere")
|
|
207
|
+
atm.def_dim("altitude", 30)
|
|
208
|
+
atm.def_var("pressure", :float, ["time", "altitude"], units: "hPa")
|
|
209
|
+
atm.def_var("temperature", :float, ["time", "altitude"], units: "K")
|
|
210
|
+
|
|
211
|
+
# Timeline coordinate at the root
|
|
212
|
+
nc.def_var("time", :int, ["time"],
|
|
213
|
+
units: "hours since 2024-01-01T00:00:00Z", calendar: "standard")
|
|
214
|
+
|
|
215
|
+
# Write some data
|
|
216
|
+
T = 6
|
|
217
|
+
nc["time"] = CArray.time_series("2024-01-01T00:00:00Z", count: T, unit: :h)
|
|
218
|
+
|
|
219
|
+
T.times do |t|
|
|
220
|
+
ocean["temperature", t, nil] = CArray.float32(50) { |z| 25.0 - z * 0.4 }
|
|
221
|
+
ocean["salinity", t, nil] = CArray.float32(50) { |z| 35.0 + z * 0.01 }
|
|
222
|
+
atm["pressure", t, nil] = CArray.float32(30) { |z| 1013.0 * Math.exp(-z / 8.0) }
|
|
223
|
+
atm["temperature", t, nil] = CArray.float32(30) { |z| 288.15 - z * 6.5 }
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Read via full path:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
NC::File.open("model.nc") do |nc|
|
|
232
|
+
nc["/ocean/temperature"].shape # => [6, 50]
|
|
233
|
+
nc["/atmosphere/pressure"][0, nil] # 1-D CArray of shape [30]
|
|
234
|
+
nc.group("ocean").dims["depth"].length # => 50
|
|
235
|
+
nc["/time"].to_ca # CATime (CF codec applied)
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 7. Inspect a mystery file quickly
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
NC::File.open("mystery.nc") do |nc|
|
|
243
|
+
puts "format: #{nc.format}"
|
|
244
|
+
puts "conventions: #{nc.att('Conventions') || '(none)'}"
|
|
245
|
+
puts "codec: #{nc.codec.class.name.split('::').last}"
|
|
246
|
+
puts
|
|
247
|
+
|
|
248
|
+
puts "dimensions:"
|
|
249
|
+
nc.dims.each { |n, d| puts " #{n} = #{d.length}#{' (unlimited)' if d.unlimited?}" }
|
|
250
|
+
puts
|
|
251
|
+
|
|
252
|
+
puts "variables:"
|
|
253
|
+
nc.vars.each do |n, v|
|
|
254
|
+
puts " #{v.data_type} #{n}#{v.shape.inspect}"
|
|
255
|
+
v.atts.each { |k, val| puts " :#{k} = #{val.inspect}" }
|
|
256
|
+
end
|
|
257
|
+
puts
|
|
258
|
+
|
|
259
|
+
puts "global attributes:"
|
|
260
|
+
nc.atts.each { |k, v| puts " :#{k} = #{v.inspect}" }
|
|
261
|
+
|
|
262
|
+
unless nc.groups.empty?
|
|
263
|
+
puts
|
|
264
|
+
puts "groups:"
|
|
265
|
+
nc.groups.each { |n, g| puts " #{g.path} (#{g.vars.size} vars, #{g.groups.size} subgroups)" }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 8. Round-trip decoded vs raw for a single variable
|
|
271
|
+
|
|
272
|
+
Handy when a file behaves unexpectedly and you want to see what the codec is doing:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
NC::File.open("data.nc") do |nc|
|
|
276
|
+
decoded = nc["temp"].to_ca
|
|
277
|
+
puts "decoded: mean = #{decoded.mean}, masked = #{decoded.count_masked}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
NC::File.open("data.nc", codec: :identity) do |nc|
|
|
281
|
+
raw = nc["temp"].to_ca
|
|
282
|
+
puts "raw: mean = #{raw.mean}, min = #{raw.min}, max = #{raw.max}"
|
|
283
|
+
puts "attrs: #{nc['temp'].atts.inspect}"
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 9. Write a small file with a bit-exact custom codec
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
# A minimal codec that only masks _FillValue but skips scale/offset/time.
|
|
291
|
+
class OnlyFillCodec
|
|
292
|
+
def decode(var, ca)
|
|
293
|
+
fill = var.att("_FillValue") or return ca
|
|
294
|
+
out = ca.copy
|
|
295
|
+
out[:eq, fill] = UNDEF
|
|
296
|
+
out
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def encode(var, ca)
|
|
300
|
+
return ca unless ca.is_a?(CArray) && ca.has_mask?
|
|
301
|
+
fill = var.att("_FillValue") or
|
|
302
|
+
raise ArgumentError, "no _FillValue to encode masked cells"
|
|
303
|
+
ca.strip_mask(fill)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
NC::File.open("packed.nc", codec: OnlyFillCodec.new) do |nc|
|
|
308
|
+
raw_with_mask = nc["temp"].to_ca # packed short values, fill cells masked,
|
|
309
|
+
# no scale/offset applied
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The codec object just has to respond to `decode(var, ca)` and `encode(var, ca)`. See the [Codecs chapter](04_codec.md) for the full interface and how to register a codec globally.
|
|
314
|
+
|
|
315
|
+
## 10. Existing-file inspection with `ncdump`-like output
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
def cdl_summary(nc)
|
|
319
|
+
puts "netcdf {"
|
|
320
|
+
puts "dimensions:"
|
|
321
|
+
nc.dims.each { |n, d|
|
|
322
|
+
len = d.unlimited? ? "UNLIMITED" : d.length.to_s
|
|
323
|
+
puts " #{n} = #{len};"
|
|
324
|
+
}
|
|
325
|
+
puts "variables:"
|
|
326
|
+
nc.vars.each { |n, v|
|
|
327
|
+
dims = v.dims.map(&:name).join(", ")
|
|
328
|
+
puts " #{v.data_type} #{n}(#{dims});"
|
|
329
|
+
v.atts.each { |k, val|
|
|
330
|
+
puts " #{n}:#{k} = #{val.inspect};"
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
puts "// global attributes:"
|
|
334
|
+
nc.atts.each { |k, v| puts " :#{k} = #{v.inspect};" }
|
|
335
|
+
nc.groups.each { |gname, g|
|
|
336
|
+
puts
|
|
337
|
+
puts "group: #{gname} {"
|
|
338
|
+
cdl_summary(g)
|
|
339
|
+
puts "} // group #{gname}"
|
|
340
|
+
}
|
|
341
|
+
puts "}"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
NC::File.open("mystery.nc") { |nc| cdl_summary(nc) }
|
|
345
|
+
```
|