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
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
module CA
|
|
2
|
+
module NetCDF
|
|
3
|
+
# A NetCDF variable: a lazy handle. Indexing issues a hyperslab read
|
|
4
|
+
# and runs the file's {Codec} (decode on read, encode on write).
|
|
5
|
+
# Delegates unknown methods to the decoded CArray (`var.mean`,
|
|
6
|
+
# `var.sum`, …).
|
|
7
|
+
#
|
|
8
|
+
# @example Reading
|
|
9
|
+
# var = nc["temp"]
|
|
10
|
+
# var[] # whole variable, decoded
|
|
11
|
+
# var[0, 0..9, ..] # hyperslab
|
|
12
|
+
# var.to_ca # whole variable
|
|
13
|
+
# var.raw # bypass codec
|
|
14
|
+
#
|
|
15
|
+
# @example Writing
|
|
16
|
+
# nc["temp"][] = ca # whole variable
|
|
17
|
+
# nc["temp"][0, ..] = row # hyperslab
|
|
18
|
+
class Var
|
|
19
|
+
|
|
20
|
+
include CA::NetCDF
|
|
21
|
+
|
|
22
|
+
# @param group [Group] containing group (root or subgroup)
|
|
23
|
+
# @param varid [Integer] NetCDF variable id within `group`
|
|
24
|
+
def initialize(group, varid)
|
|
25
|
+
@group = group
|
|
26
|
+
@ncid = group.ncid
|
|
27
|
+
@varid = varid
|
|
28
|
+
@name = nc_inq_varname(@ncid, @varid)
|
|
29
|
+
@nc_type = nc_inq_vartype(@ncid, @varid)
|
|
30
|
+
@dimids = nc_inq_vardimid(@ncid, @varid)
|
|
31
|
+
# Per axis: may a write extend it? The dim can live in an ancestor
|
|
32
|
+
# group, and a group can hold several unlimited dims (netcdf-4).
|
|
33
|
+
@unlimited = @dimids.map { |d| group.dim_by_id(d)&.unlimited? || false }
|
|
34
|
+
refresh_shape
|
|
35
|
+
@atts = read_attributes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @!attribute [r] name
|
|
39
|
+
# @return [String]
|
|
40
|
+
# @!attribute [r] atts
|
|
41
|
+
# @return [Hash{String => Object}] frozen attribute map
|
|
42
|
+
# @!attribute [r] group
|
|
43
|
+
# @return [Group]
|
|
44
|
+
# @!attribute [r] varid
|
|
45
|
+
# @return [Integer]
|
|
46
|
+
attr_reader :name, :atts, :group, :varid
|
|
47
|
+
|
|
48
|
+
# Current shape. Always freshly inquired — the unlimited dim may have
|
|
49
|
+
# grown since this Var was created.
|
|
50
|
+
# @return [Array<Integer>]
|
|
51
|
+
def shape = refresh_shape
|
|
52
|
+
|
|
53
|
+
# @return [Integer] number of dimensions
|
|
54
|
+
def ndim = shape.length
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] true if this is a 0-D scalar variable
|
|
57
|
+
def scalar? = @dimids.empty?
|
|
58
|
+
|
|
59
|
+
# @return [Integer] on-disk `NC_*` type constant
|
|
60
|
+
def nc_type = @nc_type
|
|
61
|
+
|
|
62
|
+
# @return [Symbol] CArray `data_type` symbol
|
|
63
|
+
def data_type = NC_TO_CA[@nc_type]
|
|
64
|
+
|
|
65
|
+
# @return [File] root file
|
|
66
|
+
def file = @group.file
|
|
67
|
+
|
|
68
|
+
# Look up a variable attribute.
|
|
69
|
+
# @param name [#to_s]
|
|
70
|
+
# @return [Object, nil]
|
|
71
|
+
def att(name) = @atts[name.to_s]
|
|
72
|
+
|
|
73
|
+
# The variable's dimensions, in order. A dim may live in an ancestor
|
|
74
|
+
# group (netcdf-4 dim inheritance), so resolve by name up the tree.
|
|
75
|
+
# @return [Array<Dim>]
|
|
76
|
+
def dims = @dimids.map { |d| @group.resolve_dim(nc_inq_dimname(@ncid, d)) }
|
|
77
|
+
|
|
78
|
+
# @return [Object] the codec object (Identity / CF / COARDS / custom)
|
|
79
|
+
def codec = @group.codec
|
|
80
|
+
|
|
81
|
+
# @!group Read
|
|
82
|
+
|
|
83
|
+
# Read (a hyperslab of) this variable with codec decoding applied.
|
|
84
|
+
# Accepts any CArray indexing form (range, integer, `..`, `nil`).
|
|
85
|
+
# Like a write, a read leaves define mode first, so a file being
|
|
86
|
+
# created can be read back between definitions.
|
|
87
|
+
#
|
|
88
|
+
# @param argv [Array] CArray-style index arguments
|
|
89
|
+
# @return [CArray, Numeric, CATime::Element] decoded values
|
|
90
|
+
def [](*argv)
|
|
91
|
+
@group.ensure_data_mode
|
|
92
|
+
refresh_shape
|
|
93
|
+
return read_scalar_var(argv) if scalar?
|
|
94
|
+
info = CArray.scan_index(@shape, argv)
|
|
95
|
+
if info.type == CA_REG_METHOD_CALL
|
|
96
|
+
return decode(nc_get_var(@ncid, @varid))[*argv]
|
|
97
|
+
end
|
|
98
|
+
decode(read_raw(info, argv))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Read the whole variable with codec decoding.
|
|
102
|
+
#
|
|
103
|
+
# The values are read out of the file into a fresh array, which shares
|
|
104
|
+
# nothing with the variable. `writable: true` — a caller asking for an
|
|
105
|
+
# array whose writes reach the source — is therefore refused rather
|
|
106
|
+
# than answered with one that would swallow them; write with
|
|
107
|
+
# `v[…] = …` instead.
|
|
108
|
+
#
|
|
109
|
+
# @param writable [Boolean] refused when true (see above).
|
|
110
|
+
# @return [CArray, Numeric] decoded values (scalar for 0-D vars)
|
|
111
|
+
# @raise [RuntimeError] when `writable: true` is given.
|
|
112
|
+
def to_ca(writable: false)
|
|
113
|
+
if writable
|
|
114
|
+
raise "#{self.class}#to_ca reads into a new array; it can't " \
|
|
115
|
+
"satisfy `writable: true' (write with `v[...] = ...')"
|
|
116
|
+
end
|
|
117
|
+
@group.ensure_data_mode
|
|
118
|
+
refresh_shape
|
|
119
|
+
return read_scalar_var([]) if scalar?
|
|
120
|
+
decode(nc_get_var(@ncid, @varid))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Raw values — the public descent point that bypasses the codec.
|
|
124
|
+
#
|
|
125
|
+
# `v[…]` and `v.to_ca` compose as
|
|
126
|
+
#
|
|
127
|
+
# v[…] == v.codec.decode(v, v.raw(…))
|
|
128
|
+
# v.to_ca == v.codec.decode(v, v.raw)
|
|
129
|
+
#
|
|
130
|
+
# so a caller (or an upper convenience layer) can reach the on-disk
|
|
131
|
+
# bytes without going through `instance_variable_get(:@varid)`. No
|
|
132
|
+
# `_FillValue → UNDEF`, no scale/offset, no `_Unsigned` reinterpret,
|
|
133
|
+
# no CATime. Accepts the same indexing as {#[]}.
|
|
134
|
+
#
|
|
135
|
+
# @param argv [Array] CArray-style index arguments
|
|
136
|
+
# @return [CArray, Numeric] raw on-disk values
|
|
137
|
+
def raw(*argv)
|
|
138
|
+
@group.ensure_data_mode
|
|
139
|
+
refresh_shape
|
|
140
|
+
if scalar?
|
|
141
|
+
raise ArgumentError, "scalar variable #{@name.inspect} takes no index" unless argv.empty?
|
|
142
|
+
return nc_get_var(@ncid, @varid)[0]
|
|
143
|
+
end
|
|
144
|
+
return nc_get_var(@ncid, @varid) if argv.empty?
|
|
145
|
+
info = CArray.scan_index(@shape, argv)
|
|
146
|
+
case info.type
|
|
147
|
+
when CA_REG_METHOD_CALL
|
|
148
|
+
nc_get_var(@ncid, @varid)[*argv]
|
|
149
|
+
else
|
|
150
|
+
read_raw(info, argv)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @!endgroup
|
|
155
|
+
|
|
156
|
+
# @!group Write
|
|
157
|
+
|
|
158
|
+
# Write (to a hyperslab of) this variable with codec encoding
|
|
159
|
+
# applied.
|
|
160
|
+
#
|
|
161
|
+
# @param argv [Array] index arguments followed by the value; the
|
|
162
|
+
# final positional argument is the value to assign
|
|
163
|
+
# @return [Object] the assigned value
|
|
164
|
+
def []=(*argv)
|
|
165
|
+
value = argv.pop
|
|
166
|
+
@group.ensure_data_mode
|
|
167
|
+
refresh_shape
|
|
168
|
+
return write_scalar_var(argv, value) if scalar?
|
|
169
|
+
|
|
170
|
+
info, scan_shape = scan_index_for_write(argv)
|
|
171
|
+
case info.type
|
|
172
|
+
when CA_REG_ADDRESS
|
|
173
|
+
put_var1(addr_to_index(info.index[0], scan_shape), encoded_scalar(value))
|
|
174
|
+
when CA_REG_POINT
|
|
175
|
+
put_var1(info.index, encoded_scalar(value))
|
|
176
|
+
when CA_REG_FLATTEN
|
|
177
|
+
nc_put_var(@ncid, @varid, encoded_array(value))
|
|
178
|
+
when CA_REG_ALL
|
|
179
|
+
ca = encoded_array(value)
|
|
180
|
+
# Use ca.shape (not @shape) so a whole-variable write into an
|
|
181
|
+
# unlimited-dim variable can extend the unlimited axis; every
|
|
182
|
+
# fixed axis must match, or part of the variable would be left
|
|
183
|
+
# as it was.
|
|
184
|
+
check_whole_write_shape(ca.shape)
|
|
185
|
+
start = ca.shape.map { 0 }
|
|
186
|
+
nc_put_vara(@ncid, @varid, start, ca.shape, ca)
|
|
187
|
+
when CA_REG_BLOCK
|
|
188
|
+
start, count, stride, _, reversed = block_spec(info)
|
|
189
|
+
ca = reverse_axes(encoded_array(value, count), count, reversed)
|
|
190
|
+
if stride.all? { |s| s == 1 }
|
|
191
|
+
nc_put_vara(@ncid, @varid, start, count, ca)
|
|
192
|
+
else
|
|
193
|
+
nc_put_vars(@ncid, @varid, start, count, stride, ca)
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
raise ArgumentError, "unsupported index for writing #{@name}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# @!endgroup
|
|
201
|
+
|
|
202
|
+
# Define a variable attribute, typed as {Group#def_var} types its
|
|
203
|
+
# attributes: a CArray keeps its own type, and a packed-space
|
|
204
|
+
# attribute (`_FillValue`, `valid_range`, …) given as a Ruby value
|
|
205
|
+
# takes the variable's on-disk type.
|
|
206
|
+
# @param name [#to_s]
|
|
207
|
+
# @param value [Object] Ruby value or CArray
|
|
208
|
+
# @return [Object] the value written
|
|
209
|
+
def def_att(name, value)
|
|
210
|
+
@group.__transaction__ do
|
|
211
|
+
put_var_att(@ncid, @varid, name.to_s, value, @nc_type)
|
|
212
|
+
end
|
|
213
|
+
@atts = read_attributes
|
|
214
|
+
value
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @return [String]
|
|
218
|
+
def inspect = "#<#{self.class} #{@name}#{shape.inspect}>"
|
|
219
|
+
|
|
220
|
+
# CArray methods that change their receiver without a `!` / `=` in
|
|
221
|
+
# the name. Delegated, they would change a throwaway copy.
|
|
222
|
+
# @api private
|
|
223
|
+
IN_PLACE_METHODS = %i[
|
|
224
|
+
fill unmask paste inherit_mask inherit_mask_replace attach
|
|
225
|
+
load_binary set_attr
|
|
226
|
+
].freeze
|
|
227
|
+
|
|
228
|
+
# @api private
|
|
229
|
+
def respond_to_missing?(id, include_private = false)
|
|
230
|
+
(CArray.method_defined?(id) && !in_place?(id)) || super
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Delegate unknown methods to {#to_ca} — enables `var.mean`, `var.sum`, …
|
|
234
|
+
#
|
|
235
|
+
# A method that changes its receiver (`fill`, `seq!`, `mask=`, …) is
|
|
236
|
+
# refused: it would change the fresh array {#to_ca} returns, not the
|
|
237
|
+
# file. Write with `v[…] = …`, or call it on `v.to_ca` explicitly.
|
|
238
|
+
# @raise [NoMethodError] for an in-place CArray method
|
|
239
|
+
def method_missing(id, *argv, &block)
|
|
240
|
+
return super unless CArray.method_defined?(id)
|
|
241
|
+
if in_place?(id)
|
|
242
|
+
raise NoMethodError.new(
|
|
243
|
+
"#{self.class}##{id} would change a copy read from the file, not " \
|
|
244
|
+
"the variable; write with `v[...] = ...' (or call it on v.to_ca)",
|
|
245
|
+
id)
|
|
246
|
+
end
|
|
247
|
+
to_ca.__send__(id, *argv, &block)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @api private
|
|
251
|
+
private def in_place?(id)
|
|
252
|
+
id.end_with?("!", "=") || IN_PLACE_METHODS.include?(id)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# ---- helpers -------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
# The array read path goes straight through the codec; the scalar
|
|
258
|
+
# (var1) read path wraps the value in a 1-elem carrier, decodes it
|
|
259
|
+
# with the same codec pipeline, and unwraps. That way
|
|
260
|
+
# `_FillValue → UNDEF`, scale/offset, and CF time → `CATime::Element`
|
|
261
|
+
# all work identically for `v[i, j]` and for `v[:, :]`. The 1-cell
|
|
262
|
+
# allocation is cheap enough for interactive use and keeps the
|
|
263
|
+
# semantics uniform.
|
|
264
|
+
# @api private
|
|
265
|
+
private def decode(raw)
|
|
266
|
+
return codec.decode(self, raw) if raw.is_a?(CArray)
|
|
267
|
+
wrapped = CArray.new(data_type, [1]) { raw }
|
|
268
|
+
codec.decode(self, wrapped)[0] # CArray[0] / CATime[0]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Encode a value to a CArray usable by `nc_put_var{,a,s,m}`.
|
|
272
|
+
# - CArray input: pass through the codec.
|
|
273
|
+
# - Numeric / UNDEF / Element: broadcast-fill this variable's shape,
|
|
274
|
+
# run through the codec. This is how `nc["v"] = 42.0` fills the
|
|
275
|
+
# whole variable with a scalar (matches CArray's `ca[] = 42.0`
|
|
276
|
+
# idiom); a block write passes the block's `shape`, which may reach
|
|
277
|
+
# past the end of an unlimited dim. Only used by array-shaped
|
|
278
|
+
# writes (`CA_REG_ALL` / `_BLOCK` / `_FLATTEN`); scalar writes go
|
|
279
|
+
# through {#encoded_scalar}.
|
|
280
|
+
# @api private
|
|
281
|
+
private def encoded_array(value, shape = @shape)
|
|
282
|
+
return codec.encode(self, value) if value.is_a?(CArray)
|
|
283
|
+
ca = broadcast_scalar_to_shape(value, shape)
|
|
284
|
+
codec.encode(self, ca)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Encode a value to a plain Numeric usable by `nc_put_var1`. Accepts
|
|
288
|
+
# a Numeric, UNDEF, a `CATime[delta]::Element`, OR a 1-element /
|
|
289
|
+
# 0-D CArray (the point-write case where the user handed us an
|
|
290
|
+
# array).
|
|
291
|
+
#
|
|
292
|
+
# Numerics are wrapped in a type that preserves precision (int64 for
|
|
293
|
+
# Integer, float64 for Float / UNDEF) so scale/offset can operate on
|
|
294
|
+
# the true value; the array-write path never truncates for the same
|
|
295
|
+
# reason. A float value going to an integer variable is rounded at
|
|
296
|
+
# the end to match `encode_scale`'s CF §8.1 round rule.
|
|
297
|
+
# @api private
|
|
298
|
+
private def encoded_scalar(value)
|
|
299
|
+
wrapped = case value
|
|
300
|
+
when CATime::Element
|
|
301
|
+
CA_INT64([value.value]).time(unit: value.unit)
|
|
302
|
+
when CATimedelta::Element
|
|
303
|
+
CA_INT64([value.value]).timedelta(unit: value.unit)
|
|
304
|
+
when CArray
|
|
305
|
+
unless value.elements == 1
|
|
306
|
+
raise ArgumentError,
|
|
307
|
+
"point write to #{@name} needs a single value; " \
|
|
308
|
+
"got a #{value.shape.inspect} array"
|
|
309
|
+
end
|
|
310
|
+
value # trust caller's dtype
|
|
311
|
+
else
|
|
312
|
+
w = CArray.new(preserve_scalar_type(value), [1])
|
|
313
|
+
w[0] = value # handles UNDEF, coerces numerics
|
|
314
|
+
w
|
|
315
|
+
end
|
|
316
|
+
encoded = codec.encode(self, wrapped)
|
|
317
|
+
# Consistency with the array-write path: for an integer target, a
|
|
318
|
+
# float-typed CArray gets rounded (encode_scale does this when
|
|
319
|
+
# scale/offset is present; do it unconditionally here so the point
|
|
320
|
+
# path also rounds when the user writes 3.7 to an :int var).
|
|
321
|
+
if encoded.is_a?(CArray) &&
|
|
322
|
+
INTEGER_TYPES.include?(data_type) &&
|
|
323
|
+
!INTEGER_TYPES.include?(encoded.data_type)
|
|
324
|
+
encoded = encoded.round
|
|
325
|
+
end
|
|
326
|
+
# If the codec did NOT encode the mask into a sentinel (Identity,
|
|
327
|
+
# or CF for a var without _FillValue), guard against silently
|
|
328
|
+
# writing CArray's internal UNDEF representation to disk.
|
|
329
|
+
if encoded.is_a?(CArray) && encoded.has_mask? && encoded.is_masked[0]
|
|
330
|
+
raise ArgumentError,
|
|
331
|
+
"cannot point-write UNDEF to #{@name}: the codec did not " \
|
|
332
|
+
"encode the mask. Add a _FillValue attribute and open with " \
|
|
333
|
+
"the CF codec, or write a real value."
|
|
334
|
+
end
|
|
335
|
+
encoded[0]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# @api private
|
|
339
|
+
INTEGER_TYPES = %i[int8 uint8 int16 uint16 int32 uint32 int64 uint64].freeze
|
|
340
|
+
|
|
341
|
+
# Widest natural type for a Ruby scalar, so wrapping a Float in a
|
|
342
|
+
# 1-elem CArray doesn't truncate against an int variable's data_type.
|
|
343
|
+
# @api private
|
|
344
|
+
private def preserve_scalar_type(value)
|
|
345
|
+
case value
|
|
346
|
+
when Float then :float64
|
|
347
|
+
when Integer then :int64
|
|
348
|
+
else data_type # UNDEF, nil, ... (mask-capable of any type)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Build a shape-matching CArray filled with `value`. CArray's
|
|
353
|
+
# arity-0 block form is a documented single-evaluation fill idiom.
|
|
354
|
+
# @api private
|
|
355
|
+
private def broadcast_scalar_to_shape(value, shape)
|
|
356
|
+
if shape.empty? # 0-D scalar variable
|
|
357
|
+
w = CArray.new(data_type, [1])
|
|
358
|
+
w[0] = value
|
|
359
|
+
w
|
|
360
|
+
else
|
|
361
|
+
CArray.new(data_type, shape) { value }
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# 0-D scalar variables have shape [] — `CArray.scan_index` doesn't
|
|
366
|
+
# accept that, and `nc_get_var` / `nc_put_var` deal in a 1-element
|
|
367
|
+
# buffer. Reads / writes route through `nc_get_var` / `nc_put_var`
|
|
368
|
+
# and drop the extra dimension for the caller.
|
|
369
|
+
# @api private
|
|
370
|
+
private def read_scalar_var(argv)
|
|
371
|
+
unless argv.empty?
|
|
372
|
+
raise ArgumentError, "scalar variable #{@name.inspect} takes no index"
|
|
373
|
+
end
|
|
374
|
+
raw = nc_get_var(@ncid, @varid) # 1-element CArray
|
|
375
|
+
decoded = codec.decode(self, raw)
|
|
376
|
+
decoded[0] # scalar Numeric / UNDEF / Element
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# @api private
|
|
380
|
+
private def write_scalar_var(argv, value)
|
|
381
|
+
unless argv.empty?
|
|
382
|
+
raise ArgumentError, "scalar variable #{@name.inspect} takes no index"
|
|
383
|
+
end
|
|
384
|
+
ca = encoded_array(value) # 1-element CArray of var type
|
|
385
|
+
nc_put_var(@ncid, @varid, ca)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# @api private
|
|
389
|
+
private def read_raw(info, argv)
|
|
390
|
+
case info.type
|
|
391
|
+
when CA_REG_ALL
|
|
392
|
+
nc_get_var(@ncid, @varid)
|
|
393
|
+
when CA_REG_FLATTEN
|
|
394
|
+
nc_get_var(@ncid, @varid)[nil]
|
|
395
|
+
when CA_REG_ADDRESS
|
|
396
|
+
get_var1(addr_to_index(info.index[0]))
|
|
397
|
+
when CA_REG_POINT
|
|
398
|
+
get_var1(info.index)
|
|
399
|
+
when CA_REG_BLOCK
|
|
400
|
+
start, count, stride, kept, reversed = block_spec(info)
|
|
401
|
+
out = if stride.all? { |s| s == 1 }
|
|
402
|
+
nc_get_vara(@ncid, @varid, start, count)
|
|
403
|
+
else
|
|
404
|
+
nc_get_vars(@ncid, @varid, start, count, stride)
|
|
405
|
+
end
|
|
406
|
+
out = reverse_axes(out, count, reversed)
|
|
407
|
+
# drop point-indexed axes (size 1) by reshaping to the kept shape
|
|
408
|
+
(kept.empty? || kept.length == count.length) ? out : out.reshape(*kept)
|
|
409
|
+
when CA_REG_SELECT, CA_REG_GRID
|
|
410
|
+
nc_get_var(@ncid, @varid)[*argv]
|
|
411
|
+
else
|
|
412
|
+
raise ArgumentError, "unsupported index for reading #{@name}"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# One element. An NC_CHAR element travels as a 1-char String in the
|
|
417
|
+
# netcdf gem's var1 calls; here it is the byte, as it is in an array
|
|
418
|
+
# read (NC_CHAR maps to :uint8).
|
|
419
|
+
# @api private
|
|
420
|
+
private def get_var1(index)
|
|
421
|
+
v = nc_get_var1(@ncid, @varid, index)
|
|
422
|
+
@nc_type == NC_CHAR && v.is_a?(String) ? (v.getbyte(0) || 0) : v
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# @api private
|
|
426
|
+
private def put_var1(index, value)
|
|
427
|
+
value = Integer(value).chr if @nc_type == NC_CHAR && value.is_a?(Numeric)
|
|
428
|
+
nc_put_var1(@ncid, @varid, index, value)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# @api private
|
|
432
|
+
private def check_whole_write_shape(shape)
|
|
433
|
+
fits = shape.length == @shape.length &&
|
|
434
|
+
shape.each_index.all? { |i| @unlimited[i] || shape[i] == @shape[i] }
|
|
435
|
+
return if fits
|
|
436
|
+
raise ArgumentError,
|
|
437
|
+
"whole-variable write to #{@name} needs shape #{@shape.inspect}" \
|
|
438
|
+
"#{' (any length on an unlimited axis)' if @unlimited.any?}; " \
|
|
439
|
+
"got #{shape.inspect}"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# -> [start, count, stride, kept_shape, reversed]; kept_shape drops
|
|
443
|
+
# point axes. libnetcdf takes only a positive stride, so a reversed
|
|
444
|
+
# range (`(9..0).step(-1)`) becomes the same cells walked forward,
|
|
445
|
+
# with its axis flagged in `reversed` for {#reverse_axes}.
|
|
446
|
+
# @api private
|
|
447
|
+
private def block_spec(info)
|
|
448
|
+
start = []; count = []; stride = []; kept = []; reversed = []
|
|
449
|
+
info.index.each do |idx|
|
|
450
|
+
if idx.is_a?(Array)
|
|
451
|
+
first, n, step = idx
|
|
452
|
+
if step.negative?
|
|
453
|
+
first += (n - 1) * step
|
|
454
|
+
step = -step
|
|
455
|
+
end
|
|
456
|
+
start << first; count << n; stride << step; kept << n
|
|
457
|
+
reversed << idx[2].negative?
|
|
458
|
+
else
|
|
459
|
+
start << idx; count << 1; stride << 1; reversed << false
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
[start, count, stride, kept, reversed]
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# `ca` (of `count` elements) laid out as `count`, with the flagged
|
|
466
|
+
# axes reversed — the same both ways, so it serves read and write.
|
|
467
|
+
# @api private
|
|
468
|
+
private def reverse_axes(ca, count, reversed)
|
|
469
|
+
return ca unless reversed.any?
|
|
470
|
+
ca = ca.to_ca.reshape(*count) unless ca.shape == count
|
|
471
|
+
ca[*reversed.each_with_index.map { |r, i| r ? [count[i] - 1, count[i], -1] : nil }].to_ca
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# @api private
|
|
475
|
+
private def addr_to_index(addr, shape = @shape)
|
|
476
|
+
index = Array.new(shape.length)
|
|
477
|
+
(shape.length - 1).downto(0) do |i|
|
|
478
|
+
index[i] = addr % shape[i]
|
|
479
|
+
addr /= shape[i]
|
|
480
|
+
end
|
|
481
|
+
index
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Refresh `@shape` from the file and return it — the unlimited dim
|
|
485
|
+
# can grow between reads/writes, so the length cached at open time
|
|
486
|
+
# goes stale. Public callers (like {#shape}) receive the returned
|
|
487
|
+
# array; internal callers rely on `@shape` being updated as a side
|
|
488
|
+
# effect.
|
|
489
|
+
# @api private
|
|
490
|
+
private def refresh_shape
|
|
491
|
+
@shape = @dimids.map { |d| nc_inq_dimlen(@ncid, d) }
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Resolve a write index -> [info, shape it was resolved against].
|
|
495
|
+
#
|
|
496
|
+
# Indices are resolved against the true `@shape` whenever they fit,
|
|
497
|
+
# so a negative index or an open range means what it means on read.
|
|
498
|
+
# Only an index past the end of an unlimited dimension (libnetcdf
|
|
499
|
+
# grows it on the actual `put_var*` call) falls back to a shape with
|
|
500
|
+
# that axis capped at a huge length. An index that resolves
|
|
501
|
+
# differently under two such caps depends on the axis length — a
|
|
502
|
+
# negative index or an open range reaching past the end — and has no
|
|
503
|
+
# meaning there, so it is refused instead of landing near 2**31.
|
|
504
|
+
# @api private
|
|
505
|
+
private def scan_index_for_write(argv)
|
|
506
|
+
[CArray.scan_index(@shape, argv), @shape]
|
|
507
|
+
rescue IndexError
|
|
508
|
+
raise unless @unlimited.any?
|
|
509
|
+
shape = shape_for_write(INT_MAX_SCAN)
|
|
510
|
+
info = CArray.scan_index(shape, argv)
|
|
511
|
+
unless CArray.scan_index(shape_for_write(INT_MAX_SCAN - 1), argv) == info
|
|
512
|
+
raise IndexError,
|
|
513
|
+
"index #{argv.inspect} for writing #{@name} is out of range " \
|
|
514
|
+
"for shape #{@shape.inspect}; only a non-negative index can " \
|
|
515
|
+
"extend the unlimited dimension"
|
|
516
|
+
end
|
|
517
|
+
[info, shape]
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# `@shape` with every unlimited axis replaced by `cap`.
|
|
521
|
+
# @api private
|
|
522
|
+
INT_MAX_SCAN = 2**31 - 1
|
|
523
|
+
private def shape_for_write(cap)
|
|
524
|
+
@unlimited.each_with_index.map { |u, i| u ? cap : @shape[i] }
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# @api private
|
|
528
|
+
private def read_attributes
|
|
529
|
+
natts = nc_inq_varnatts(@ncid, @varid)
|
|
530
|
+
atts = {}
|
|
531
|
+
natts.times do |i|
|
|
532
|
+
name = nc_inq_attname(@ncid, @varid, i)
|
|
533
|
+
atts[name] = nc_get_att(@ncid, @varid, name)
|
|
534
|
+
end
|
|
535
|
+
atts.freeze
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|