carray-netcdf 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/carray/netcdf.rb CHANGED
@@ -3,7 +3,7 @@ module CA
3
3
  #
4
4
  # CA::NetCDF ← this module
5
5
  #
6
- # Layer 1 (this module's functions): a CArray-flavored view of the netcdf
6
+ # The low-level API (this module's functions): a CArray-flavored view of the netcdf
7
7
  # gem's `nc_*` API. `extend self` exposes every method both as a module
8
8
  # function and as an instance method, so it is usable two ways:
9
9
  #
@@ -20,15 +20,15 @@ module CA
20
20
  # speak CArray; scalar (`var1`), definition, inquiry, attribute and group
21
21
  # calls fall through to {::NetCDF} verbatim.
22
22
  #
23
- # Layer 1 is raw values only: no convention decoding, no mask handling.
23
+ # The low-level API is raw values only: no convention decoding, no mask handling.
24
24
  # Reads return unmasked CArrays; writing a masked CArray raises. The mask
25
- # / CF story lives in layer 2 (the {File} / {Var} / {Codec} classes).
25
+ # / CF story lives in the high-level API (the {File} / {Var} / {Codec} classes).
26
26
  module NetCDF
27
27
 
28
28
  include ::NetCDF
29
29
  extend self
30
30
 
31
- # @!group Layer 1 — array reads
31
+ # @!group Low-level API — array reads
32
32
 
33
33
  # Read an entire variable.
34
34
  #
@@ -82,17 +82,29 @@ module CA
82
82
  # @param buffer [CArray, MemoryView, nil]
83
83
  # @return [CArray]
84
84
  def nc_get_varm(ncid, varid, start, count, stride, imap, buffer = nil)
85
+ # A buffer this function allocates is laid out row-major over
86
+ # `count`; any other imap needs a buffer laid out for it.
87
+ if buffer.nil? && imap != row_major_imap(count)
88
+ raise ArgumentError,
89
+ "nc_get_varm with imap #{imap.inspect} needs a buffer; without one, " \
90
+ "imap must be row-major over count (#{row_major_imap(count).inspect})"
91
+ end
85
92
  ::NetCDF.nc_get_varm(ncid, varid, start, count, stride, imap,
86
93
  buffer || new_buffer(ncid, varid, count))
87
94
  end
88
95
 
96
+ # @api private
97
+ private def row_major_imap(count)
98
+ count.each_index.map { |i| count[(i + 1)..].inject(1, :*) }
99
+ end
100
+
89
101
  # @!endgroup
90
102
 
91
- # @!group Layer 1 — array writes
103
+ # @!group Low-level API — array writes
92
104
  #
93
105
  # Accept a CArray as the buffer. netcdf requires a C-contiguous buffer,
94
106
  # so non-entity views are materialized; masked input is rejected (fill
95
- # encoding is layer 2's job).
107
+ # encoding is the high-level API's job).
96
108
 
97
109
  # Write an entire variable.
98
110
  #
@@ -141,6 +153,11 @@ module CA
141
153
  _, _, base, _, klass = nc_inq_user_type(ncid, xtype)
142
154
  xtype = base if klass == NC_ENUM
143
155
  end
156
+ if xtype == NC_STRING
157
+ raise NotImplementedError,
158
+ "no CArray type for an NC_STRING variable; read it with " \
159
+ "NetCDF.nc_get_var, which returns Strings"
160
+ end
144
161
  data_type = NC_TO_CA[xtype] or
145
162
  raise NotImplementedError, "no CArray type to read nc_type #{xtype} into; pass a buffer"
146
163
  CArray.new(data_type, shape.empty? ? [1] : shape)
@@ -148,14 +165,18 @@ module CA
148
165
 
149
166
  # A C-contiguous, unmasked CArray ready to hand to libnetcdf:
150
167
  # materialize non-entity views, refuse masked input (encoding a fill
151
- # value is the convention layer's decision, not layer 1's).
168
+ # value is the codec's decision, not the low-level API's).
152
169
  # @api private
153
170
  private def writable_buffer(ca)
154
171
  return ca unless ca.is_a?(CArray)
172
+ # A mask with nothing masked is only carried along: drop it, so the
173
+ # values go out as they are.
174
+ return ca.strip_mask(0) if ca.has_mask? && ! ca.any_masked?
155
175
  if ca.has_mask?
156
176
  raise ArgumentError,
157
- "cannot write a masked CArray at layer 1; strip the mask " \
158
- "(e.g. #strip_mask(fill)) in the convention layer first"
177
+ "cannot write a masked CArray without a codec to encode the mask; " \
178
+ "strip the mask first (e.g. #strip_mask(fill)), or write through " \
179
+ "a codec such as CF (a Conventions attribute naming CF selects it)"
159
180
  end
160
181
  ca.entity? ? ca : ca.copy
161
182
  end
@@ -192,7 +213,8 @@ module CA
192
213
  # @param type [Symbol, String, Integer]
193
214
  # @return [Integer] an `NC_*` type constant
194
215
  # @raise [ArgumentError] on an unrecognized type name
195
- def nc_type_for(type)
216
+ # @api private
217
+ private def nc_type_for(type)
196
218
  return type if type.is_a?(Integer)
197
219
  TYPE_TO_NC[type.to_s.to_sym] or
198
220
  raise ArgumentError, "unknown NetCDF variable type: #{type.inspect}"
@@ -224,7 +246,8 @@ module CA
224
246
  # @param value [Object] Ruby value or CArray
225
247
  # @param nc_type [Integer] target `NC_*` type
226
248
  # @return [void]
227
- def put_att_typed(ncid, varid, name, value, nc_type)
249
+ # @api private
250
+ private def put_att_typed(ncid, varid, name, value, nc_type)
228
251
  suffix = PUT_ATT_SUFFIX[nc_type]
229
252
  return nc_put_att(ncid, varid, name, value) unless suffix
230
253
  fn = "nc_put_att_#{suffix}"
@@ -250,12 +273,69 @@ module CA
250
273
  # @api private
251
274
  private def put_att_takes_xtype?(fn) = PUT_ATT_XTYPE[fn]
252
275
 
276
+ # Marks an attribute {#readable_att} could not read.
277
+ # @api private
278
+ UNREADABLE_ATT = Object.new.freeze
279
+
280
+ # An attribute's value, or {UNREADABLE_ATT} with a warning when the
281
+ # netcdf gem cannot read its type (an enum-typed `_FillValue`, as
282
+ # netCDF4-python writes, is one): one such attribute should not stop
283
+ # the whole file from opening.
284
+ # @api private
285
+ private def readable_att(ncid, varid, name, owner)
286
+ value = nc_get_att(ncid, varid, name)
287
+ # A C writer's text often ends in a NUL; ncdump and xarray drop it.
288
+ # Done on the bytes: the text need not be valid UTF-8.
289
+ return value unless value.is_a?(String) && value.end_with?("\0")
290
+ String.new(value.b.sub(/\0+\z/n, ""), encoding: value.encoding)
291
+ rescue NotImplementedError => e
292
+ warn "carray-netcdf: attribute #{name.inspect} of #{owner} skipped: #{e.message}"
293
+ UNREADABLE_ATT
294
+ end
295
+
296
+ # An attribute value frozen through, so a caller changing what `atts`
297
+ # returned cannot change what the codec reads next.
298
+ # @api private
299
+ private def deep_freeze(value)
300
+ value.each { |x| x.freeze } if value.is_a?(Array)
301
+ value.freeze
302
+ end
303
+
304
+ # Whether an `_Unsigned` attribute value says "true".
305
+ # @api private
306
+ private def unsigned_flag?(value) = value.to_s.strip.downcase == "true"
307
+
308
+ # Signed bits per `NC_*` type, for {#signed_wrap}.
309
+ # @api private
310
+ SIGNED_BITS = { NC_BYTE => 8, NC_SHORT => 16, NC_INT => 32, NC_INT64 => 64 }.freeze
311
+
312
+ # An `_Unsigned` variable stores unsigned values in a signed type, and
313
+ # its fill / range attributes are read in unsigned space; one given in
314
+ # unsigned space (255 for a byte) is written as the signed value with
315
+ # the same bits (-1).
316
+ # @api private
317
+ private def signed_wrap(value, var_type)
318
+ bits = SIGNED_BITS[var_type] or return value
319
+ wrap = ->(x) { x.is_a?(Integer) && x >= 2**(bits - 1) && x < 2**bits ? x - 2**bits : x }
320
+ value.is_a?(Array) ? value.map(&wrap) : wrap.(value)
321
+ end
322
+
323
+ # A CArray attribute value as the flat Array the netcdf gem takes. An
324
+ # attribute is one-dimensional, and has no masked elements.
325
+ # @api private
326
+ private def attribute_values(name, ca)
327
+ if ca.any_masked?
328
+ raise ArgumentError, "attribute #{name.inspect} has masked elements"
329
+ end
330
+ ca.to_a.flatten
331
+ end
332
+
253
333
  # Write a variable attribute the way {Group#def_var} does:
254
334
  #
255
- # - a CArray keeps its own type (`_FillValue: CA_SHORT([-1])` on a
256
- # float variable is a short attribute);
257
- # - a Ruby value for a {PACKED_SPACE_ATTS} name takes the variable's
258
- # on-disk type `var_type`, which libnetcdf requires of `_FillValue`;
335
+ # - a value for a {PACKED_SPACE_ATTS} name, a CArray included, takes
336
+ # the variable's on-disk type `var_type`, which libnetcdf requires of
337
+ # `_FillValue` and CF asks of the others;
338
+ # - another CArray keeps its own type;
259
339
  # - anything else is left to the generic `nc_put_att` inference.
260
340
  #
261
341
  # @param ncid [Integer]
@@ -265,9 +345,11 @@ module CA
265
345
  # @param var_type [Integer] the variable's `NC_*` type
266
346
  # @return [void]
267
347
  # @api private
268
- def put_var_att(ncid, varid, name, value, var_type)
348
+ private def put_var_att(ncid, varid, name, value, var_type, unsigned: false)
349
+ value = attribute_values(name, value) if value.is_a?(CArray) && PACKED_SPACE_ATTS.include?(name)
350
+ value = signed_wrap(value, var_type) if unsigned && PACKED_SPACE_ATTS.include?(name)
269
351
  if value.is_a?(CArray)
270
- put_att_typed(ncid, varid, name, value.to_a, TYPE_TO_NC[value.data_type])
352
+ put_att_typed(ncid, varid, name, attribute_values(name, value), TYPE_TO_NC[value.data_type])
271
353
  elsif PACKED_SPACE_ATTS.include?(name)
272
354
  put_att_typed(ncid, varid, name, value, var_type)
273
355
  else
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carray-netcdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - himotoyoshi
@@ -29,20 +29,20 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.1.0
32
+ version: 0.2.0
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
- version: '0.2'
35
+ version: '0.3'
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: 0.1.0
42
+ version: 0.2.0
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
- version: '0.2'
45
+ version: '0.3'
46
46
  description: |
47
47
  A NetCDF interface for CArray, built on the netcdf gem. Data is
48
48
  exchanged as CArray at every level, through Ruby's MemoryView protocol,
@@ -74,7 +74,7 @@ files:
74
74
  - docs/03_writing.md
75
75
  - docs/04_codec.md
76
76
  - docs/05_groups.md
77
- - docs/06_layer1.md
77
+ - docs/06_low_level_api.md
78
78
  - docs/07_cookbook.md
79
79
  - docs/08_cheatsheet.md
80
80
  - docs/09_cli.md
@@ -97,7 +97,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
97
97
  requirements:
98
98
  - - ">="
99
99
  - !ruby/object:Gem::Version
100
- version: 3.1.0
100
+ version: '3.2'
101
101
  required_rubygems_version: !ruby/object:Gem::Requirement
102
102
  requirements:
103
103
  - - ">="
data/docs/06_layer1.md DELETED
@@ -1,160 +0,0 @@
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`.