carray-netcdf 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71bb04b44cd8c06925d970fb1e69ebdea46b7320aeec1e3c542aca3668a66cfc
4
- data.tar.gz: 864e70e40ec87e90200e74ca97590e7449808dd8e5d012ce64aac8805182733f
3
+ metadata.gz: 6b62bd15777ba74f1034591a396904bdce84015051c9895c9f01e1793ea39685
4
+ data.tar.gz: fc6d94c6f079e92081035f999d3a12d111622202e858b64c7f06fb61e06e0d9b
5
5
  SHA512:
6
- metadata.gz: 99b7b5824c461197bbdb4b7161d59728d095976c207964d34208d560a7a14ef4c9a432ef8a76ee974ad97095fe4b89e611dd5b9e262cf25b20f734c30eb0d737
7
- data.tar.gz: d9ef82a43aaacdb8d9e0b83786d3f0d222f4738feb3fc2b3b785cc01cbcf7117ae83c5f51372831c3b80e4bfb503ab84e231917568adc9c2551133d09fab65f9
6
+ metadata.gz: fa102781a9705ea42e3a9b96ff95e7d34c4f2dc2204512d468f16866085bde6f35f2115f41d9e90a4ca4d25d424708380c7c8efeed092e7eb2f43d92c68d1210
7
+ data.tar.gz: f5841947884c2e91e72c334cc3884576778f36b71d8bf0d1aa5a537591893b5f26982a06fe5529c5e9eac7206af427131853bf973ba9a5b276cc33f851fa5fb0
data/CHANGELOG.md CHANGED
@@ -30,6 +30,77 @@ different codebase and is not covered.
30
30
  netcdf gem's. Which of those a release needs is said in the gemspec,
31
31
  and an entry says so only when the answer changes. -->
32
32
 
33
+ ## 2.1.0
34
+
35
+ - New: `def_var` and `File.create` take `zstandard:`, `bzip2:`, `szip:`
36
+ and `blosc:` beside `deflate:`. zstandard, bzip2 and blosc need
37
+ libnetcdf 4.9; a filter libnetcdf lacks raises `NetCDF::Error` rather
38
+ than writing the data uncompressed.
39
+
40
+ - New: `Var#storage_options` gives a variable's chunking, compression and
41
+ other storage settings in `def_var`'s keyword form, and
42
+ `File#netcdf4?` says whether the file can have them.
43
+
44
+ - New: `File.create(format:)` makes `:classic`, `:offset64` (CDF-2),
45
+ `:cdf5`, `:netcdf4` or `:netcdf4_classic` files. A template's format
46
+ is kept, and `netcdf4: false` wins over a NetCDF-4 template.
47
+
48
+ - New: `File#reload` / `Group#reload` read the file's dimensions,
49
+ variables, groups and attributes again, after changes made through the
50
+ low-level API.
51
+
52
+ - New: `Var#att_type` gives the `NC_*` type an attribute is stored in.
53
+
54
+ - New: a file with CF attributes but no `Conventions` (as xarray and
55
+ netCDF4-python write) warns once that it is read as stored, and
56
+ suggests `codec: :cf`.
57
+
58
+ - Change: these are private now: `CA::NetCDF.nc_type_for`,
59
+ `.put_att_typed` and `.put_var_att`; the low-level get / put calls on
60
+ `File`, `Group`, `Var` and `Dim`; `Group#copy_att_from` and
61
+ `File.atomic_target`. Pass a type name to `def_var`, call
62
+ `NetCDF.nc_put_att_*`, or call the get / put functions on `CA::NetCDF`.
63
+
64
+ - Change: attribute values from `atts` / `att`, and what `dims` / `vars`
65
+ / `groups` return, are frozen. Code that changes them in place should
66
+ copy them first.
67
+
68
+ - Change: `File.create(..., clobber: false)` on an existing file raises
69
+ `Errno::EEXIST`, also without `atomic:`; it raised `NetCDF::Error`.
70
+ Code that rescued that should rescue `Errno::EEXIST`.
71
+
72
+ - Change: needs Ruby 3.2 or later, as CArray 3.0 does, and the netcdf gem
73
+ 0.2.0 or later (below 0.3).
74
+
75
+ - Change: `File#abort`, and the block form when its block raises, undo
76
+ the definitions made since the file last left define mode; they were
77
+ kept. An aborted non-atomic `File.create` removes the file.
78
+
79
+ - Change: `def_var(template:)`, `copy_var` and `File.create(vars:)` copy
80
+ the template's storage settings when both files are NetCDF-4.
81
+ Compression options you pass replace the template's as a whole. Chunk
82
+ sizes are copied only when the dimensions have the same lengths.
83
+
84
+ - Change: float32 `scale_factor` / `add_offset` unpack in float32, as CF
85
+ asks and xarray does; they unpacked in float64.
86
+
87
+ - Change: a CArray given as `_FillValue`, `missing_value` or `valid_*` is
88
+ written in the variable's type, as a Ruby value is. A masked one
89
+ raises `ArgumentError`.
90
+
91
+ - Change: reading by an index array on an axis, and asking a variable's
92
+ size, `first` or `last`, no longer read the whole variable.
93
+
94
+ - Fix: a float written to an integer variable is rounded, and NaN
95
+ written to an integer or packed variable becomes the fill value (or
96
+ raises when there is none). 2.0.0 truncated the float and stored NaN as
97
+ `0`, so check integer data that 2.0.0 wrote from floats.
98
+
99
+ - Fix: under the standard calendar, a CF time reference before
100
+ 1582-10-15 is a Julian date, as cftime reads it, and a one- to
101
+ three-digit reference year is that year. 2.0.0 read such times wrong
102
+ (`"days since 1-1-1"` as 2001).
103
+
33
104
  ## 2.0.0
34
105
 
35
106
  A ground-up rewrite. Nothing from 1.x is carried over, so read this as a
@@ -49,12 +120,12 @@ new library that happens to keep the name rather than as an upgrade.
49
120
  binding — is included rather than modified, so a future `Numo::NetCDF`
50
121
  can sit beside it. Code written against 1.x's names does not run here.
51
122
 
52
- - New: layer 1, `CA::NetCDF`'s own `nc_*` functions. They are the
123
+ - New: the low-level API, `CA::NetCDF`'s own `nc_*` functions. They are the
53
124
  netcdf gem's API with the array I/O overridden to take and return
54
125
  CArrays; definition, inquiry, attribute, and group calls pass through
55
126
  verbatim. Usable as module functions or by `include CA::NetCDF`.
56
127
 
57
- - New: layer 2, the object-oriented surface — `File`, `Group`, `Var`,
128
+ - New: the high-level API, the object-oriented surface — `File`, `Group`, `Var`,
58
129
  `Dim`. `File.open` / `File.create`, `nc[:temp]` to read a variable,
59
130
  `nc[:temp, 0..9] = data` to write a hyperslab, lazy hyperslab reads,
60
131
  file-wide variable option defaults, and atomic writes through a
data/CONTRIBUTING.md CHANGED
@@ -46,7 +46,7 @@ And, where they apply, the things most bugs here turn on:
46
46
 
47
47
  ## Sending a fix
48
48
 
49
- Ruby 3.1 or later, CArray and the netcdf gem in the ranges the gemspec declares. Sibling checkouts of `carray` and `netcdf` beside this one are picked up automatically when they are there; otherwise the installed gems are used.
49
+ Ruby 3.2 or later, CArray and the netcdf gem in the ranges the gemspec declares. Sibling checkouts of `carray` and `netcdf` beside this one are picked up automatically when they are there; otherwise the installed gems are used.
50
50
 
51
51
  ```sh
52
52
  rake spec # the whole suite (also the default task)
@@ -66,9 +66,9 @@ Keep the commit message to a subject line and a line or two of context. Anything
66
66
 
67
67
  ## Working on the code
68
68
 
69
- `docs/` is the guide and is written to be read in order. The code is one file per class under `lib/carray/netcdf/`, with layer 1 — the `nc_*` functions that speak CArray — in `lib/carray/netcdf.rb` itself.
69
+ `docs/` is the guide and is written to be read in order. The code is one file per class under `lib/carray/netcdf/`, with the low-level API — the `nc_*` functions that speak CArray — in `lib/carray/netcdf.rb` itself.
70
70
 
71
- The line worth holding while working here: layer 1 is raw values only. No convention decoding, no mask handling; reads come back unmasked and writing a masked CArray raises. Everything about `_FillValue`, `scale_factor`, and time belongs to the codecs in layer 2.
71
+ The line worth holding while working here: the low-level API is raw values only. No convention decoding, no mask handling; reads come back unmasked and writing a masked CArray raises. Everything about `_FillValue`, `scale_factor`, and time belongs to the codecs in the high-level API.
72
72
 
73
73
  ## License
74
74
 
data/README.md CHANGED
@@ -1,25 +1,26 @@
1
1
  # carray-netcdf
2
2
 
3
- A NetCDF interface for [CArray](https://github.com/himotoyoshi/carray), built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem.
3
+ Read and write [NetCDF](https://www.unidata.ucar.edu/software/netcdf/) files from Ruby, with variable data read into and written from [CArray](https://github.com/himotoyoshi/carray)s.
4
4
 
5
- Data is exchanged as CArray at every level: reads return a CArray and writes accept one. Both go through Ruby's MemoryView protocol, so libnetcdf reads and writes the array's own memory, with no intermediate representation of the data.
5
+ CArray is a Ruby library of multi-dimensional arrays, each holding a single numeric type, with slicing, element-wise arithmetic, and statistics over the whole array or along any axis. Every element can be masked as missing, and the statistics skip masked elements.
6
6
 
7
- Two layers do this, both in the `CA::NetCDF` module. Layer 1 turns the netcdf gem's MemoryView buffers into CArray, keeping the `nc_*` functions as they are. Layer 2 builds the object-oriented interface on top: `File`, `Group`, `Var`, `Dim` and the codecs. Both are pure Ruby, on CArray 3.0; every libnetcdf call goes through the netcdf gem.
7
+ The gem is built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem, a near one-to-one binding to libnetcdf, and offers two APIs on top of it:
8
8
 
9
- Layer 2 gives you data as the file's conventions define it. For a file declaring CF or COARDS, values are decoded on the way out: `_FillValue` becomes a mask, `scale_factor` and `add_offset` are applied, and time coordinates become CATime. Variables are indexed like CArrays, and only the elements you index are read — `nc["temp"][0..9, nil, nil]` reads ten days, not the whole variable.
9
+ * **Low-level API** — the netcdf gem's `nc_*` functions, with the ones that read and write variable data taking and returning CArray.
10
+ * **High-level API** — an object-oriented API (`File`, `Group`, `Var`, `Dim`) that applies the file's conventions, and is what everyday code uses.
10
11
 
11
12
  ## Status
12
13
 
13
- 2.0 still moves: its own surface is not settled yet, so behaviour can change between releases.
14
+ The 2.0 API is not settled yet, and may change between releases.
14
15
 
15
16
  ## Features
16
17
 
17
- * **Object-oriented access to NetCDF data.** A file, a group, a variable and a dimension are objects: `File.open`, `nc["temp"]`, `var.dims`, `var.atts`, and a block form that closes the file for you.
18
- * **CArray in and out.** Reads return a CArray and writes accept one. A variable is indexed the way a CArray is, so only the slab you ask for is read, and libnetcdf works on the array's own memory rather than a copy of it.
19
- * **Conventions are decoded — CF, COARDS, or none.** Under CF or COARDS, `_FillValue` becomes a mask, `scale_factor` and `add_offset` are applied, and a time axis becomes CATime. `codec: :identity` gives you the stored values instead, and a codec of your own can take their place.
20
- * **NetCDF-4 groups.** Variables are addressed by path (`nc["/obs/temp"]`), dimensions are inherited from the groups above, and the file's codec applies inside them.
21
- * **Writing is a schema and an assignment.** `def_dim`, `def_var`, `def_att`, then `nc["temp"] = ca`. Attributes that CF requires to match the variable's type are written in that type, so other tools read the file as intended.
22
- * **Atomic writes.** `File.create(atomic: true)` writes to a temporary file in the same directory and renames it into place only when the block completes, so a failed run leaves no half-written file behind.
18
+ * **Object-oriented API.** Files, groups, variables and dimensions are objects.
19
+ * **Partial data access.** Only the part of a variable you index is read or written, so a variable larger than memory can be handled a piece at a time.
20
+ * **CF and COARDS conventions.** Files whose `Conventions` attribute names CF or COARDS are read as the conventions define them: missing values as masks, packed values unpacked, and time axes as dates and times. Writing reverses each of these.
21
+ * **Definitions and data in any order.** Dimensions, variables and attributes can be defined and data written in whatever order suits you; NetCDF's define and data modes are switched as needed.
22
+ * **Existing files as templates.** A new file can take its dimensions, global attributes and coordinate variables from an existing file, and any variable can take its definition from a variable in another file.
23
+ * **Atomic writes.** On request, a file appears under its name only once it has been written completely, so a failed run leaves no half-written file behind.
23
24
 
24
25
  ## Install
25
26
 
@@ -45,12 +46,38 @@ gem "carray-netcdf"
45
46
 
46
47
  Requires:
47
48
 
48
- - Ruby >= 3.1
49
- - carray >= 3.0.1
49
+ - Ruby >= 3.2
50
+ - libnetcdf >= 4.1, with its headers (`nc-config` or `pkg-config` on PATH)
51
+ - A C compiler, to build the netcdf gem
52
+ - carray >= 3.0.1, installed as a dependency
50
53
  - the netcdf gem, installed as a dependency
51
54
 
52
55
  ## Example
53
56
 
57
+ ### Low-level API
58
+
59
+ The `nc_*` functions that read and write variable data are on `CA::NetCDF` and take and return CArray; the others, such as opening and looking up, are called on the netcdf gem's `NetCDF` as they are. Files and variables are integer ids, and the values come back as stored, so fill values, packing and time units are yours to handle:
60
+
61
+ ```ruby
62
+ require "carray-netcdf"
63
+
64
+ NC = CA::NetCDF
65
+
66
+ ncid = NetCDF.nc_open("data.nc", NetCDF::NC_NOWRITE) # the netcdf gem, as it is
67
+ vid = NetCDF.nc_inq_varid(ncid, "temp")
68
+
69
+ day = NC.nc_get_vara(ncid, vid, [0, 0, 0], [1, 73, 144]) # a new CArray, as stored
70
+
71
+ buf = CArray.float32(73, 144)
72
+ NC.nc_get_vara(ncid, vid, [0, 0, 0], [1, 73, 144], buf) # or read into one you have
73
+
74
+ NetCDF.nc_close(ncid)
75
+ ```
76
+
77
+ ### High-level API
78
+
79
+ Files and variables by name, and a variable indexed like a CArray. The CF or COARDS conventions a file declares in its `Conventions` attribute are applied automatically — here, the `_FillValue` element comes back masked:
80
+
54
81
  ```ruby
55
82
  require "carray-netcdf"
56
83
 
@@ -65,9 +92,11 @@ NC::File.create("out.nc") do |nc|
65
92
  nc.def_var("temp", :float, ["time", "lat", "lon"],
66
93
  units: "K", _FillValue: -999.9)
67
94
 
68
- nc.def_att("Conventions", "CF-1.7")
95
+ nc.def_att("Conventions", "CF-1.7") # turns CF encoding and decoding on
69
96
 
70
- nc["temp"] = CArray.float32(1, 73, 144) { rand * 30 + 260 }
97
+ temp = CArray.float32(1, 73, 144).random(30) + 260 # random values, 260-290
98
+ temp[0, 0, 0] = UNDEF # mark one element missing
99
+ nc["temp"] = temp # written as -999.9
71
100
  end
72
101
 
73
102
  # --- read ---
@@ -76,23 +105,26 @@ NC::File.open("out.nc") do |nc|
76
105
  t.shape # => [1, 73, 144]
77
106
  t.dims.map(&:name) # => ["time", "lat", "lon"]
78
107
 
79
- day = t[0, nil, nil] # a 2-D CArray; only this slab is read
80
- day.mean # CF decoding has already been applied
108
+ day = t[0, nil, nil] # a 73 x 144 CArray; only this part is read
109
+ day.count_masked # => 1, the -999.9 came back masked
110
+ day.mean # the mean of the other 10511 elements
81
111
  end
82
112
  ```
83
113
 
84
114
  ## Documentation
85
115
 
86
- * [Introduction](docs/00_introduction.md) — why there are two layers, and what the gem leaves to CArray
116
+ * [Introduction](docs/00_introduction.md) — what the gem does, what it leaves to CArray, and how it sits on the netcdf gem
87
117
  * [Getting started](docs/01_getting_started.md) — install, then read and write a first file
88
118
  * [Reading files](docs/02_reading.md) — opening a file, finding a variable, reading all or part of it
89
119
  * [Writing files](docs/03_writing.md) — declaring the schema, writing data, changing a file later
90
120
  * [Codecs and CF conventions](docs/04_codec.md) — what gets decoded, how a codec is chosen, writing your own
91
121
  * [Groups](docs/05_groups.md) — NetCDF-4 groups, and what they inherit
92
- * [Layer 1](docs/06_layer1.md) — the `nc_*` functions with CArray in and out
122
+ * [Low-level API](docs/06_low_level_api.md) — the `nc_*` functions with CArray in and out
93
123
  * [Cookbook](docs/07_cookbook.md) — worked solutions to real tasks
94
124
  * [Cheatsheet](docs/08_cheatsheet.md) — every method and constant of the gem on one page
95
- * [Command-line tools](docs/09_cli.md) — `ncheader` and `ncvar`, which ship with the gem
125
+ * [Command-line tools](docs/09_cli.md) — `ncheader` and `ncvar`
126
+
127
+ What changed between releases is in [CHANGELOG.md](CHANGELOG.md).
96
128
 
97
129
  ## Contributing
98
130
 
@@ -106,4 +138,4 @@ carray-netcdf was designed and reviewed by a human developer; the implementation
106
138
 
107
139
  ## License
108
140
 
109
- MIT
141
+ [MIT](LICENSE)
data/bin/ncheader CHANGED
@@ -27,6 +27,15 @@ PRIMITIVE_TYPE_NAMES = {
27
27
  NetCDF::NC_STRING => "string",
28
28
  }
29
29
 
30
+ # A String as valid UTF-8, for JSON / YAML and the terminal: a byte that
31
+ # is not part of a UTF-8 character is written as \xNN (old files often
32
+ # hold Latin-1 text).
33
+ def printable_text(s)
34
+ # String.new, not dup.force_encoding: before carray 3.0.7, a string from
35
+ # CArray#dump_binary kept a stale "valid" flag through force_encoding.
36
+ String.new(s, encoding: Encoding::UTF_8).scrub { |b| b.unpack("C*").map { |c| format("\\x%02X", c) }.join }
37
+ end
38
+
30
39
  def type_name(ncid, xtype)
31
40
  PRIMITIVE_TYPE_NAMES[xtype] || NetCDF.nc_inq_type(ncid, xtype).first
32
41
  end
@@ -35,10 +44,20 @@ def collect_attrs(ncid, varid, natts)
35
44
  natts.times.map do |i|
36
45
  name = NetCDF.nc_inq_attname(ncid, varid, i)
37
46
  xtype, _len = NetCDF.nc_inq_att(ncid, varid, name)
47
+ value = begin
48
+ NetCDF.nc_get_att(ncid, varid, name)
49
+ rescue NotImplementedError
50
+ "(not readable: #{type_name(ncid, xtype)} attribute)"
51
+ end
52
+ value = case value
53
+ when String then printable_text(value).sub(/\u0000+\z/, "") # as ncdump drops it
54
+ when Array then value.map { |v| v.is_a?(String) ? printable_text(v) : v }
55
+ else value
56
+ end
38
57
  {
39
- "name" => name,
58
+ "name" => printable_text(name),
40
59
  "type" => type_name(ncid, xtype),
41
- "value" => NetCDF.nc_get_att(ncid, varid, name),
60
+ "value" => value,
42
61
  }
43
62
  end
44
63
  end
@@ -73,11 +92,13 @@ def collect_user_types(ncid)
73
92
  end
74
93
 
75
94
  def inspect_group(ncid)
76
- _, _, natts, unlimid = NetCDF.nc_inq(ncid)
95
+ _, _, natts, _ = NetCDF.nc_inq(ncid)
96
+ # Every unlimited dimension of the group (NetCDF-4 can have several).
97
+ unlimited = NetCDF.nc_inq_unlimdims(ncid)
77
98
 
78
99
  dims = NetCDF.nc_inq_dimids(ncid, false).map { |id|
79
100
  name, len = NetCDF.nc_inq_dim(ncid, id)
80
- { "name" => name, "length" => len, "unlimited" => (id == unlimid) }
101
+ { "name" => name, "length" => len, "unlimited" => unlimited.include?(id) }
81
102
  }
82
103
 
83
104
  vars = NetCDF.nc_inq_varids(ncid).map { |vid|
@@ -113,6 +134,18 @@ TYPE_SUFFIX = {
113
134
  "float" => "f", "double" => "",
114
135
  }
115
136
 
137
+ # A float32 value with the fewest digits that read back as the same
138
+ # float32, as ncdump prints it (`0.1`, not 0.10000000149011612).
139
+ def float32_text(x)
140
+ return x.to_s unless x.finite?
141
+ bits = [x].pack("f")
142
+ (1..9).each do |digits|
143
+ s = format("%.#{digits}g", x)
144
+ return s if [Float(s)].pack("f") == bits
145
+ end
146
+ x.to_s
147
+ end
148
+
116
149
  def cdl_escape(s)
117
150
  s.to_s.gsub(/[\\"\n\r\t]/, "\\" => '\\\\', '"' => '\\"',
118
151
  "\n" => '\\n', "\r" => '\\r', "\t" => '\\t')
@@ -127,7 +160,7 @@ def cdl_value(type, value)
127
160
  suffix = TYPE_SUFFIX[type] || ""
128
161
  arr.map { |v|
129
162
  if v.is_a?(Float)
130
- type == "float" ? "#{v}f" : v.to_s
163
+ type == "float" ? "#{float32_text(v)}f" : v.to_s
131
164
  else
132
165
  "#{v}#{suffix}"
133
166
  end
@@ -173,7 +206,8 @@ def emit_cdl(io, group, name, indent = 0)
173
206
  dims = v["dimensions"].empty? ? "" : "(#{v["dimensions"].join(", ")})"
174
207
  io.puts "#{pre} #{v["type"]} #{v["name"]}#{dims} ;"
175
208
  v["attributes"].each { |a|
176
- io.puts "#{pre} #{v["name"]}:#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
209
+ prefix = a["type"] == "string" ? "string " : ""
210
+ io.puts "#{pre} #{prefix}#{v["name"]}:#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
177
211
  }
178
212
  }
179
213
  end
@@ -183,7 +217,8 @@ def emit_cdl(io, group, name, indent = 0)
183
217
  label = indent.zero? ? "global attributes" : "group attributes"
184
218
  io.puts "#{pre}// #{label}:"
185
219
  group["attributes"].each { |a|
186
- io.puts "#{pre} :#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
220
+ prefix = a["type"] == "string" ? "string " : ""
221
+ io.puts "#{pre} #{prefix}:#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
187
222
  }
188
223
  end
189
224
 
@@ -218,6 +253,6 @@ end
218
253
 
219
254
  case format
220
255
  when "cdl" then emit_cdl($stdout, tree, tree["name"])
221
- when "json" then puts JSON.pretty_generate(tree)
256
+ when "json" then puts JSON.pretty_generate(tree, allow_nan: true) # NaN / Infinity as bare words
222
257
  when "yaml" then puts YAML.dump(tree)
223
258
  end
data/bin/ncvar CHANGED
@@ -35,6 +35,32 @@ TYPE_NAMES = {
35
35
  NetCDF::NC_STRING => "string",
36
36
  }
37
37
 
38
+ # A String as valid UTF-8, for JSON / YAML and the terminal: a byte that
39
+ # is not part of a UTF-8 character is written as \xNN (old files often
40
+ # hold Latin-1 text).
41
+ def printable_text(s)
42
+ # String.new, not dup.force_encoding: before carray 3.0.7, a string from
43
+ # CArray#dump_binary kept a stale "valid" flag through force_encoding.
44
+ String.new(s, encoding: Encoding::UTF_8).scrub { |b| b.unpack("C*").map { |c| format("\\x%02X", c) }.join }
45
+ end
46
+
47
+ # printable_text over a nested Array of Strings.
48
+ def deep_text(a)
49
+ a.map { |x| x.is_a?(Array) ? deep_text(x) : printable_text(x) }
50
+ end
51
+
52
+ # A float32 value with the fewest digits that read back as the same
53
+ # float32, as ncdump prints it (`0.1`, not 0.10000000149011612).
54
+ def float32_text(x)
55
+ return x.to_s unless x.finite?
56
+ bits = [x].pack("f")
57
+ (1..9).each do |digits|
58
+ s = format("%.#{digits}g", x)
59
+ return s if [Float(s)].pack("f") == bits
60
+ end
61
+ x.to_s
62
+ end
63
+
38
64
  def parse_int_list(s)
39
65
  return nil if s.nil?
40
66
  s.split(",").map { |x| Integer(x) }
@@ -77,13 +103,20 @@ begin
77
103
  shape_full = dimids.map { |d| NetCDF.nc_inq_dimlen(gid, d) }
78
104
 
79
105
  start = parse_int_list(start_arg) || [0] * shape_full.size
80
- count = parse_int_list(count_arg) || shape_full.zip(start).map { |full, s| full - s }
81
106
  stride = parse_int_list(stride_arg)
82
-
83
- abort "start/count/stride rank mismatch (var has #{shape_full.size} dims)" \
84
- unless start.size == shape_full.size &&
85
- count.size == shape_full.size &&
86
- (stride.nil? || stride.size == shape_full.size)
107
+ abort "start/stride rank mismatch (var has #{shape_full.size} dims)" \
108
+ unless start.size == shape_full.size && (stride.nil? || stride.size == shape_full.size)
109
+ start.each_with_index do |s, i|
110
+ unless (0..shape_full[i]).cover?(s)
111
+ abort "start #{s} is out of range for dimension #{i} of length #{shape_full[i]}"
112
+ end
113
+ end
114
+ # By default, the rest of each dimension, taking every stride-th element.
115
+ steps = stride || [1] * shape_full.size
116
+ abort "stride must be positive" unless steps.all?(&:positive?)
117
+ count = parse_int_list(count_arg) ||
118
+ shape_full.each_index.map { |i| (shape_full[i] - start[i] + steps[i] - 1) / steps[i] }
119
+ abort "count rank mismatch (var has #{shape_full.size} dims)" unless count.size == shape_full.size
87
120
 
88
121
  shape = count.dup
89
122
 
@@ -119,8 +152,8 @@ begin
119
152
  "type" => "string",
120
153
  "shape" => shape,
121
154
  "start" => start,
122
- "data" => data,
123
- })
155
+ "data" => data.is_a?(Array) ? deep_text(data) : printable_text(data),
156
+ }, allow_nan: true)
124
157
  end
125
158
  else
126
159
  # data is a CArray; its bytes are the native-endian stream.
@@ -135,15 +168,18 @@ begin
135
168
  puts unless bytes.end_with?("\n")
136
169
  else
137
170
  abort "type #{xtype} not supported for text output" if opaque
138
- data.to_a.flatten.each { |v| puts v }
171
+ values = data.to_a.flatten
172
+ values = values.map { |v| v.is_a?(Float) ? float32_text(v) : v } if xtype == NetCDF::NC_FLOAT
173
+ values.each { |v| puts v }
139
174
  end
140
175
  when "json"
141
176
  payload =
142
177
  if xtype == NetCDF::NC_CHAR
143
- bytes
178
+ printable_text(bytes)
144
179
  else
145
180
  abort "type #{xtype} not supported for json output" if opaque
146
- data.to_a.flatten
181
+ values = data.to_a.flatten
182
+ xtype == NetCDF::NC_FLOAT ? values.map { |v| v.is_a?(Float) && v.finite? ? Float(float32_text(v)) : v } : values
147
183
  end
148
184
  out = {
149
185
  "name" => varspec,
@@ -152,7 +188,7 @@ begin
152
188
  "start" => start,
153
189
  "data" => payload,
154
190
  }
155
- puts JSON.pretty_generate(out)
191
+ puts JSON.pretty_generate(out, allow_nan: true) # NaN / Infinity as bare words
156
192
  end
157
193
  end
158
194
  ensure
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.license = "MIT"
19
19
  spec.homepage = "https://github.com/himotoyoshi/carray-netcdf"
20
20
 
21
- spec.required_ruby_version = ">= 3.1.0"
21
+ spec.required_ruby_version = ">= 3.2"
22
22
 
23
23
  spec.files = Dir[
24
24
  "lib/**/*.rb",
@@ -34,8 +34,8 @@ Gem::Specification.new do |spec|
34
34
  spec.bindir = "bin"
35
35
  spec.executables = %w[ncheader ncvar]
36
36
 
37
- # A CArray is handed to libnetcdf as memory, so the ceiling is the next
38
- # minor on both sides.
37
+ # netcdf is capped at the next minor: its 0.x minors may change calls
38
+ # this gem makes. 0.2.0 brings the compression filter calls.
39
39
  spec.add_dependency "carray", ">= 3.0.1"
40
- spec.add_dependency "netcdf", ">= 0.1.0", "< 0.2"
40
+ spec.add_dependency "netcdf", ">= 0.2.0", "< 0.3"
41
41
  end
@@ -1,30 +1,30 @@
1
1
  # Introduction
2
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.
3
+ The carray-netcdf gem is a NetCDF interface for CArray, built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem. Using it takes some familiarity with both NetCDF and CArray. If either is new to you, Unidata's [NetCDF data model](https://docs.unidata.ucar.edu/netcdf-c/current/netcdf_data_model.html) explains dimensions, variables and attributes, the [CF conventions](https://cfconventions.org/) explain fill values, packing and time, and the [CArray](https://github.com/himotoyoshi/carray) page introduces the arrays. This guide covers what the gem adds between the two.
4
4
 
5
5
  ## What is carray-netcdf
6
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.
7
+ A NetCDF file holds arrays, and this gem hands them to you as CArrays. 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
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.
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 in its `Conventions` attribute, and when that names CF or COARDS the gem does it for you, so what comes back is a masked CArray of physical values and a time axis as CATime, CArray's array of dates and times.
10
10
 
11
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
12
 
13
- ## Two layers, one module
13
+ ## Two APIs, one module
14
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`:
15
+ The netcdf gem is a near one-to-one binding to libnetcdf, and reads and writes variable data in memory you provide. Two APIs turn that into CArray, and both live in the module `CA::NetCDF`:
16
16
 
17
17
  ```
18
- netcdf (dependency) thin libnetcdf binding, MemoryView buffers
18
+ netcdf (dependency) near one-to-one binding to libnetcdf
19
19
  ↑
20
20
  CA::NetCDF
21
- ├─ layer 1 the nc_* functions, with CArray in and out
22
- └─ layer 2 File / Group / Var / Dim, and the codecs
21
+ ├─ low-level the nc_* functions, with CArray in and out
22
+ └─ high-level File / Group / Var / Dim, and the codecs
23
23
  ```
24
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.
25
+ The low-level API keeps the `nc_*` functions as they are and only changes what they take and return. A read given no buffer returns a new CArray of the variable's type and shape; a write takes a CArray, and refuses one with masked elements, since the low-level API applies no convention. Call the get and put functions on `CA::NetCDF` and the rest on the netcdf gem's `NetCDF`. Or, for the C idiom with CArray at both ends, mix `CA::NetCDF` into a class of your own and call all of them bare.
26
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.
27
+ The high-level API is built on the low-level one and is the everyday surface: a file is an object, a variable is indexed like a CArray, and a codec — the object that applies a convention such as CF — decodes values when reading and encodes them when writing.
28
28
 
29
29
  ## Convention in this guide
30
30
 
@@ -35,38 +35,27 @@ require "carray-netcdf"
35
35
  NC = CA::NetCDF
36
36
  ```
37
37
 
38
- Requiring the gem loads CArray, the netcdf gem, and both layers.
38
+ Requiring the gem loads CArray, the netcdf gem, and both APIs.
39
39
 
40
40
  ## What the gem covers
41
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.
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. For a file that declares CF or COARDS, values pass through a codec in both directions, so `_FillValue`, `scale_factor`, `add_offset` and CF time are handled when reading and when writing. A file can be created atomically, or with its schema copied from an existing file.
43
43
 
44
44
  ## Threads and Ractors
45
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.
46
+ The GVL is released while variable data is read or written, 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
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.
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
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.
50
+ This gem 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
+ The [cookbook](07_cookbook.md#11-read-in-one-thread-compute-in-another) shows a reader thread feeding a thread that computes.
51
53
 
52
54
  ## Beyond this gem
53
55
 
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.
56
+ Two parts of CF are left to you, or to a library built on this one:
55
57
 
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.
58
+ - **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.
57
59
  - **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
 
60
61
  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