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 +4 -4
- data/CHANGELOG.md +73 -2
- data/CONTRIBUTING.md +3 -3
- data/README.md +53 -21
- data/bin/ncheader +43 -8
- data/bin/ncvar +48 -12
- data/carray-netcdf.gemspec +4 -4
- data/docs/00_introduction.md +19 -30
- data/docs/01_getting_started.md +16 -25
- data/docs/02_reading.md +26 -26
- data/docs/03_writing.md +53 -44
- data/docs/04_codec.md +74 -82
- data/docs/05_groups.md +5 -5
- data/docs/06_low_level_api.md +161 -0
- data/docs/07_cookbook.md +167 -126
- data/docs/08_cheatsheet.md +77 -52
- data/docs/09_cli.md +4 -4
- data/lib/carray/netcdf/codec.rb +223 -29
- data/lib/carray/netcdf/dim.rb +8 -5
- data/lib/carray/netcdf/file.rb +184 -57
- data/lib/carray/netcdf/group.rb +301 -43
- data/lib/carray/netcdf/var.rb +311 -31
- data/lib/carray/netcdf/version.rb +1 -1
- data/lib/carray/netcdf.rb +99 -17
- metadata +7 -7
- data/docs/06_layer1.md +0 -160
data/lib/carray/netcdf/var.rb
CHANGED
|
@@ -18,6 +18,9 @@ module CA
|
|
|
18
18
|
class Var
|
|
19
19
|
|
|
20
20
|
include CA::NetCDF
|
|
21
|
+
# The low-level calls are this class's helpers, not its interface.
|
|
22
|
+
private :nc_get_var, :nc_get_vara, :nc_get_vars, :nc_get_varm,
|
|
23
|
+
:nc_put_var, :nc_put_vara, :nc_put_vars, :nc_put_varm
|
|
21
24
|
|
|
22
25
|
# @param group [Group] containing group (root or subgroup)
|
|
23
26
|
# @param varid [Integer] NetCDF variable id within `group`
|
|
@@ -53,6 +56,34 @@ module CA
|
|
|
53
56
|
# @return [Integer] number of dimensions
|
|
54
57
|
def ndim = shape.length
|
|
55
58
|
|
|
59
|
+
# Sizes asked of the file, not by reading the variable (a CArray
|
|
60
|
+
# method forwarded to {#to_ca} would read all of it).
|
|
61
|
+
# @return [Integer]
|
|
62
|
+
def elements = shape.inject(1, :*)
|
|
63
|
+
# @return [Integer]
|
|
64
|
+
def size = elements
|
|
65
|
+
# @return [Integer, nil] the length of the first dimension
|
|
66
|
+
def dim0 = shape[0]
|
|
67
|
+
# @return [Integer, nil] the length of the second dimension
|
|
68
|
+
def dim1 = shape[1]
|
|
69
|
+
# @return [Integer, nil] the length of the third dimension
|
|
70
|
+
def dim2 = shape[2]
|
|
71
|
+
# @return [Integer, nil] the length of the fourth dimension
|
|
72
|
+
def dim3 = shape[3]
|
|
73
|
+
|
|
74
|
+
# The first / last element, read as a point.
|
|
75
|
+
# @return [Numeric, UNDEF, CATime::Element, nil]
|
|
76
|
+
def first
|
|
77
|
+
return self[] if scalar?
|
|
78
|
+
elements.zero? ? nil : self[0]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Numeric, UNDEF, CATime::Element, nil]
|
|
82
|
+
def last
|
|
83
|
+
return self[] if scalar?
|
|
84
|
+
elements.zero? ? nil : self[elements - 1]
|
|
85
|
+
end
|
|
86
|
+
|
|
56
87
|
# @return [Boolean] true if this is a 0-D scalar variable
|
|
57
88
|
def scalar? = @dimids.empty?
|
|
58
89
|
|
|
@@ -60,7 +91,17 @@ module CA
|
|
|
60
91
|
def nc_type = @nc_type
|
|
61
92
|
|
|
62
93
|
# @return [Symbol] CArray `data_type` symbol
|
|
63
|
-
|
|
94
|
+
# An enum variable has its base type's; `nil` for a type with no
|
|
95
|
+
# CArray counterpart (`NC_STRING`, opaque, ...).
|
|
96
|
+
def data_type
|
|
97
|
+
return @data_type if defined?(@data_type)
|
|
98
|
+
t = @nc_type
|
|
99
|
+
if t >= NC_FIRSTUSERTYPEID
|
|
100
|
+
_, _, base, _, klass = nc_inq_user_type(@ncid, t)
|
|
101
|
+
t = base if klass == NC_ENUM
|
|
102
|
+
end
|
|
103
|
+
@data_type = NC_TO_CA[t]
|
|
104
|
+
end
|
|
64
105
|
|
|
65
106
|
# @return [File] root file
|
|
66
107
|
def file = @group.file
|
|
@@ -70,14 +111,86 @@ module CA
|
|
|
70
111
|
# @return [Object, nil]
|
|
71
112
|
def att(name) = @atts[name.to_s]
|
|
72
113
|
|
|
114
|
+
# The `NC_*` type an attribute is stored in, or nil if there is none.
|
|
115
|
+
# @param name [#to_s]
|
|
116
|
+
# @return [Integer, nil]
|
|
117
|
+
def att_type(name)
|
|
118
|
+
return nil unless @atts.key?(name.to_s)
|
|
119
|
+
@group.file.ensure_open
|
|
120
|
+
nc_inq_atttype(@ncid, @varid, name.to_s)
|
|
121
|
+
end
|
|
122
|
+
|
|
73
123
|
# The variable's dimensions, in order. A dim may live in an ancestor
|
|
74
124
|
# group (netcdf-4 dim inheritance), so resolve by name up the tree.
|
|
75
125
|
# @return [Array<Dim>]
|
|
76
|
-
|
|
126
|
+
# A dimension is found by its id, so a same-named one defined later
|
|
127
|
+
# in a subgroup does not stand in for it.
|
|
128
|
+
def dims
|
|
129
|
+
@group.file.ensure_open
|
|
130
|
+
@dimids.map { |d| @group.dim_by_id(d) || @group.resolve_dim(nc_inq_dimname(@ncid, d)) }
|
|
131
|
+
end
|
|
77
132
|
|
|
78
133
|
# @return [Object] the codec object (Identity / CF / COARDS / custom)
|
|
79
134
|
def codec = @group.codec
|
|
80
135
|
|
|
136
|
+
# The variable's storage and filter settings, in the keyword form
|
|
137
|
+
# {Group#def_var} takes, so that another variable can be defined
|
|
138
|
+
# with the same ones. Only what differs from a new variable's
|
|
139
|
+
# default is listed: chunk sizes, the compressors and shuffle,
|
|
140
|
+
# fletcher32, a byte order other than the host's, and no_fill. A
|
|
141
|
+
# file in a classic format has none of these, and gives `{}`.
|
|
142
|
+
# @return [Hash{Symbol => Object}]
|
|
143
|
+
def storage_options
|
|
144
|
+
file.ensure_open
|
|
145
|
+
return {} unless file.netcdf4?
|
|
146
|
+
opts = {}
|
|
147
|
+
storage, sizes = nc_inq_var_chunking(@ncid, @varid)
|
|
148
|
+
case storage
|
|
149
|
+
when NC_CHUNKED then opts[:chunks] = sizes
|
|
150
|
+
when NC_COMPACT then opts[:chunks] = :compact
|
|
151
|
+
end
|
|
152
|
+
shuffle, deflate, level = nc_inq_var_deflate(@ncid, @varid)
|
|
153
|
+
opts[:deflate] = level if deflate
|
|
154
|
+
opts[:shuffle] = true if shuffle
|
|
155
|
+
if respond_to?(:nc_inq_var_zstandard, true)
|
|
156
|
+
has, level = nc_inq_var_zstandard(@ncid, @varid)
|
|
157
|
+
opts[:zstandard] = level if has
|
|
158
|
+
end
|
|
159
|
+
if respond_to?(:nc_inq_var_bzip2, true)
|
|
160
|
+
has, level = nc_inq_var_bzip2(@ncid, @varid)
|
|
161
|
+
opts[:bzip2] = level if has
|
|
162
|
+
end
|
|
163
|
+
if respond_to?(:nc_inq_var_szip, true)
|
|
164
|
+
mask, pixels = nc_inq_var_szip(@ncid, @varid)
|
|
165
|
+
if mask != 0
|
|
166
|
+
coding = (mask & NC_SZIP_NN) != 0 ? :nn : :ec
|
|
167
|
+
opts[:szip] = { coding: coding, pixels_per_block: pixels }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
if respond_to?(:nc_inq_var_blosc, true)
|
|
171
|
+
has, compressor, level, _blocksize, shuffle = nc_inq_var_blosc(@ncid, @varid)
|
|
172
|
+
if has
|
|
173
|
+
# blocksize is left out: libnetcdf's blosc filter stores the
|
|
174
|
+
# chunk's byte size in its place, not the value given.
|
|
175
|
+
opts[:blosc] = { compressor: BLOSC_COMPRESSOR_NAMES.fetch(compressor, compressor),
|
|
176
|
+
level: level,
|
|
177
|
+
shuffle: BLOSC_SHUFFLE_NAMES.fetch(shuffle, shuffle) }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
opts[:fletcher32] = true if nc_inq_var_fletcher32(@ncid, @varid)
|
|
181
|
+
endian = nc_inq_var_endian(@ncid, @varid)
|
|
182
|
+
if endian != NC_ENDIAN_NATIVE && endian != HOST_ENDIAN
|
|
183
|
+
opts[:endian] = (endian == NC_ENDIAN_BIG) ? :big : :little
|
|
184
|
+
end
|
|
185
|
+
no_fill = begin
|
|
186
|
+
nc_inq_var_fill(@ncid, @varid)[0]
|
|
187
|
+
rescue NotImplementedError
|
|
188
|
+
false # NC_STRING: no fill setting to read
|
|
189
|
+
end
|
|
190
|
+
opts[:no_fill] = true if no_fill
|
|
191
|
+
return opts
|
|
192
|
+
end
|
|
193
|
+
|
|
81
194
|
# @!group Read
|
|
82
195
|
|
|
83
196
|
# Read (a hyperslab of) this variable with codec decoding applied.
|
|
@@ -108,7 +221,8 @@ module CA
|
|
|
108
221
|
#
|
|
109
222
|
# @param writable [Boolean] refused when true (see above).
|
|
110
223
|
# @return [CArray, Numeric] decoded values (scalar for 0-D vars)
|
|
111
|
-
# @raise [RuntimeError] when `writable: true` is given
|
|
224
|
+
# @raise [RuntimeError] when `writable: true` is given, as CArray's
|
|
225
|
+
# own to_ca does.
|
|
112
226
|
def to_ca(writable: false)
|
|
113
227
|
if writable
|
|
114
228
|
raise "#{self.class}#to_ca reads into a new array; it can't " \
|
|
@@ -174,7 +288,9 @@ module CA
|
|
|
174
288
|
when CA_REG_POINT
|
|
175
289
|
put_var1(info.index, encoded_scalar(value))
|
|
176
290
|
when CA_REG_FLATTEN
|
|
177
|
-
|
|
291
|
+
ca = encoded_array(value)
|
|
292
|
+
check_write_elements(ca, @shape.inject(1, :*))
|
|
293
|
+
nc_put_var(@ncid, @varid, ca)
|
|
178
294
|
when CA_REG_ALL
|
|
179
295
|
ca = encoded_array(value)
|
|
180
296
|
# Use ca.shape (not @shape) so a whole-variable write into an
|
|
@@ -185,8 +301,10 @@ module CA
|
|
|
185
301
|
start = ca.shape.map { 0 }
|
|
186
302
|
nc_put_vara(@ncid, @varid, start, ca.shape, ca)
|
|
187
303
|
when CA_REG_BLOCK
|
|
188
|
-
start, count, stride,
|
|
189
|
-
|
|
304
|
+
start, count, stride, kept, reversed = block_spec(info)
|
|
305
|
+
encoded = encoded_array(value, count)
|
|
306
|
+
check_block_write_shape(encoded, kept) if value.is_a?(CArray)
|
|
307
|
+
ca = reverse_axes(encoded, count, reversed)
|
|
190
308
|
if stride.all? { |s| s == 1 }
|
|
191
309
|
nc_put_vara(@ncid, @varid, start, count, ca)
|
|
192
310
|
else
|
|
@@ -208,23 +326,30 @@ module CA
|
|
|
208
326
|
# @return [Object] the value written
|
|
209
327
|
def def_att(name, value)
|
|
210
328
|
@group.__transaction__ do
|
|
211
|
-
put_var_att(@ncid, @varid, name.to_s, value, @nc_type)
|
|
329
|
+
put_var_att(@ncid, @varid, name.to_s, value, @nc_type, unsigned: unsigned_flag?(@atts["_Unsigned"]))
|
|
212
330
|
end
|
|
213
331
|
@atts = read_attributes
|
|
214
332
|
value
|
|
215
333
|
end
|
|
216
334
|
|
|
217
335
|
# @return [String]
|
|
218
|
-
def inspect
|
|
336
|
+
def inspect
|
|
337
|
+
return "#<#{self.class} #{@name} (closed)>" if @group.file.closed?
|
|
338
|
+
"#<#{self.class} #{@name}#{shape.inspect}>"
|
|
339
|
+
end
|
|
219
340
|
|
|
220
341
|
# CArray methods that change their receiver without a `!` / `=` in
|
|
221
342
|
# the name. Delegated, they would change a throwaway copy.
|
|
222
343
|
# @api private
|
|
223
344
|
IN_PLACE_METHODS = %i[
|
|
224
|
-
fill unmask paste inherit_mask inherit_mask_replace attach
|
|
345
|
+
fill unmask invert_mask paste inherit_mask inherit_mask_replace attach
|
|
225
346
|
load_binary set_attr
|
|
226
347
|
].freeze
|
|
227
348
|
|
|
349
|
+
# Operators that end in `=` but only compare.
|
|
350
|
+
# @api private
|
|
351
|
+
COMPARISON_METHODS = %i[== != <= >= ===].freeze
|
|
352
|
+
|
|
228
353
|
# @api private
|
|
229
354
|
def respond_to_missing?(id, include_private = false)
|
|
230
355
|
(CArray.method_defined?(id) && !in_place?(id)) || super
|
|
@@ -236,7 +361,7 @@ module CA
|
|
|
236
361
|
# refused: it would change the fresh array {#to_ca} returns, not the
|
|
237
362
|
# file. Write with `v[…] = …`, or call it on `v.to_ca` explicitly.
|
|
238
363
|
# @raise [NoMethodError] for an in-place CArray method
|
|
239
|
-
def method_missing(id, *argv, &block)
|
|
364
|
+
private def method_missing(id, *argv, &block)
|
|
240
365
|
return super unless CArray.method_defined?(id)
|
|
241
366
|
if in_place?(id)
|
|
242
367
|
raise NoMethodError.new(
|
|
@@ -249,6 +374,7 @@ module CA
|
|
|
249
374
|
|
|
250
375
|
# @api private
|
|
251
376
|
private def in_place?(id)
|
|
377
|
+
return false if COMPARISON_METHODS.include?(id)
|
|
252
378
|
id.end_with?("!", "=") || IN_PLACE_METHODS.include?(id)
|
|
253
379
|
end
|
|
254
380
|
|
|
@@ -279,9 +405,13 @@ module CA
|
|
|
279
405
|
# through {#encoded_scalar}.
|
|
280
406
|
# @api private
|
|
281
407
|
private def encoded_array(value, shape = @shape)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
408
|
+
ca = case value
|
|
409
|
+
when CArray then value
|
|
410
|
+
when CATime::Element then element_array(value, shape)
|
|
411
|
+
else broadcast_scalar_to_shape(value, shape)
|
|
412
|
+
end
|
|
413
|
+
refuse_timedelta(ca)
|
|
414
|
+
round_for_integer_variable(codec.encode(self, ca))
|
|
285
415
|
end
|
|
286
416
|
|
|
287
417
|
# Encode a value to a plain Numeric usable by `nc_put_var1`. Accepts
|
|
@@ -300,7 +430,7 @@ module CA
|
|
|
300
430
|
when CATime::Element
|
|
301
431
|
CA_INT64([value.value]).time(unit: value.unit)
|
|
302
432
|
when CATimedelta::Element
|
|
303
|
-
|
|
433
|
+
refuse_timedelta(value)
|
|
304
434
|
when CArray
|
|
305
435
|
unless value.elements == 1
|
|
306
436
|
raise ArgumentError,
|
|
@@ -313,16 +443,7 @@ module CA
|
|
|
313
443
|
w[0] = value # handles UNDEF, coerces numerics
|
|
314
444
|
w
|
|
315
445
|
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
|
|
446
|
+
encoded = round_for_integer_variable(codec.encode(self, wrapped))
|
|
326
447
|
# If the codec did NOT encode the mask into a sentinel (Identity,
|
|
327
448
|
# or CF for a var without _FillValue), guard against silently
|
|
328
449
|
# writing CArray's internal UNDEF representation to disk.
|
|
@@ -335,30 +456,92 @@ module CA
|
|
|
335
456
|
encoded[0]
|
|
336
457
|
end
|
|
337
458
|
|
|
459
|
+
# A float-typed CArray bound for an integer variable is rounded, as
|
|
460
|
+
# `encode_scale` rounds a packed one (CF §8.1), rather than left to
|
|
461
|
+
# libnetcdf's conversion, which truncates. Shared by the array and
|
|
462
|
+
# point write paths so `3.7` lands as `4` either way.
|
|
463
|
+
# @api private
|
|
464
|
+
private def round_for_integer_variable(encoded)
|
|
465
|
+
if encoded.is_a?(CArray) &&
|
|
466
|
+
INTEGER_TYPES.include?(data_type) &&
|
|
467
|
+
!INTEGER_TYPES.include?(encoded.data_type)
|
|
468
|
+
# A NaN left here (no codec to make it the fill) would be cast to
|
|
469
|
+
# a valid-looking 0.
|
|
470
|
+
if encoded.float? && encoded.is_nan.any
|
|
471
|
+
raise ArgumentError,
|
|
472
|
+
"cannot write NaN to integer variable #{@name}; give it a " \
|
|
473
|
+
"_FillValue under a CF or COARDS codec, or replace the NaNs"
|
|
474
|
+
end
|
|
475
|
+
return encoded.round
|
|
476
|
+
end
|
|
477
|
+
encoded
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# @api private
|
|
481
|
+
HOST_ENDIAN = ([1].pack("S") == [1].pack("v")) ? NC_ENDIAN_LITTLE : NC_ENDIAN_BIG
|
|
482
|
+
|
|
483
|
+
# `nc_inq_var_blosc` values back to the names `def_var(blosc:)` takes
|
|
484
|
+
# (empty when libnetcdf is older than 4.9 and has no blosc constants).
|
|
485
|
+
# @api private
|
|
486
|
+
BLOSC_COMPRESSOR_NAMES = %i[lz lz4 lz4hc snappy zlib zstd].filter_map { |k|
|
|
487
|
+
c = "BLOSC_#{k.upcase}"
|
|
488
|
+
[::NetCDF.const_get(c), k] if ::NetCDF.const_defined?(c)
|
|
489
|
+
}.to_h.freeze
|
|
490
|
+
# @api private
|
|
491
|
+
BLOSC_SHUFFLE_NAMES = { NOSHUFFLE: :none, SHUFFLE: :byte, BITSHUFFLE: :bit }.filter_map { |c, k|
|
|
492
|
+
[::NetCDF.const_get("BLOSC_#{c}"), k] if ::NetCDF.const_defined?("BLOSC_#{c}")
|
|
493
|
+
}.to_h.freeze
|
|
494
|
+
|
|
338
495
|
# @api private
|
|
339
496
|
INTEGER_TYPES = %i[int8 uint8 int16 uint16 int32 uint32 int64 uint64].freeze
|
|
340
497
|
|
|
341
498
|
# 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
|
|
499
|
+
# 1-elem CArray doesn't truncate against an int variable's data_type,
|
|
500
|
+
# and an Integer can be unpacked or reinterpreted before it is
|
|
501
|
+
# narrowed. An Integer outside int64 keeps the variable's type
|
|
502
|
+
# (uint64), and so does anything for an NC_CHAR or enum variable,
|
|
503
|
+
# whose buffers libnetcdf takes only in their own type.
|
|
343
504
|
# @api private
|
|
505
|
+
INT64_RANGE = (-2**63)..(2**63 - 1)
|
|
344
506
|
private def preserve_scalar_type(value)
|
|
507
|
+
return data_type if @nc_type == NC_CHAR || @nc_type >= NC_FIRSTUSERTYPEID
|
|
345
508
|
case value
|
|
346
509
|
when Float then :float64
|
|
347
|
-
when Integer then :int64
|
|
510
|
+
when Integer then INT64_RANGE.cover?(value) ? :int64 : data_type
|
|
348
511
|
else data_type # UNDEF, nil, ... (mask-capable of any type)
|
|
349
512
|
end
|
|
350
513
|
end
|
|
351
514
|
|
|
515
|
+
# A CATime::Element repeated over `shape`, as a CATime.
|
|
516
|
+
# @api private
|
|
517
|
+
private def element_array(elem, shape)
|
|
518
|
+
CArray.int64(*(shape.empty? ? [1] : shape)) { elem.value }.time(unit: elem.unit)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# A duration has no CF encoding here: say so rather than writing its
|
|
522
|
+
# ticks in whatever unit it happens to have.
|
|
523
|
+
# @api private
|
|
524
|
+
private def refuse_timedelta(value)
|
|
525
|
+
return unless value.is_a?(CATimedelta) || value.is_a?(CATimedelta::Element)
|
|
526
|
+
raise ArgumentError,
|
|
527
|
+
"cannot write a CATimedelta to #{@name}; write its count in the " \
|
|
528
|
+
"variable's units (e.g. td.ticks)"
|
|
529
|
+
end
|
|
530
|
+
|
|
352
531
|
# Build a shape-matching CArray filled with `value`. CArray's
|
|
353
532
|
# arity-0 block form is a documented single-evaluation fill idiom.
|
|
354
533
|
# @api private
|
|
355
534
|
private def broadcast_scalar_to_shape(value, shape)
|
|
535
|
+
# A Float goes in as float64 and an Integer as int64, so the codec
|
|
536
|
+
# unpacks the true value, an unsigned one is reinterpreted before
|
|
537
|
+
# it is narrowed, and an integer variable gets a Float rounded.
|
|
538
|
+
type = preserve_scalar_type(value)
|
|
356
539
|
if shape.empty? # 0-D scalar variable
|
|
357
|
-
w = CArray.new(
|
|
540
|
+
w = CArray.new(type, [1])
|
|
358
541
|
w[0] = value
|
|
359
542
|
w
|
|
360
543
|
else
|
|
361
|
-
CArray.new(
|
|
544
|
+
CArray.new(type, shape) { value }
|
|
362
545
|
end
|
|
363
546
|
end
|
|
364
547
|
|
|
@@ -381,6 +564,11 @@ module CA
|
|
|
381
564
|
unless argv.empty?
|
|
382
565
|
raise ArgumentError, "scalar variable #{@name.inspect} takes no index"
|
|
383
566
|
end
|
|
567
|
+
if value.is_a?(CArray) && value.elements != 1
|
|
568
|
+
raise ArgumentError,
|
|
569
|
+
"scalar variable #{@name.inspect} needs a single value; " \
|
|
570
|
+
"got a #{value.shape.inspect} array"
|
|
571
|
+
end
|
|
384
572
|
ca = encoded_array(value) # 1-element CArray of var type
|
|
385
573
|
nc_put_var(@ncid, @varid, ca)
|
|
386
574
|
end
|
|
@@ -406,18 +594,79 @@ module CA
|
|
|
406
594
|
out = reverse_axes(out, count, reversed)
|
|
407
595
|
# drop point-indexed axes (size 1) by reshaping to the kept shape
|
|
408
596
|
(kept.empty? || kept.length == count.length) ? out : out.reshape(*kept)
|
|
409
|
-
when
|
|
597
|
+
when CA_REG_GRID
|
|
598
|
+
read_grid(argv) || nc_get_var(@ncid, @varid)[*argv]
|
|
599
|
+
when CA_REG_MAPPING
|
|
600
|
+
read_mapping(argv[0]) || nc_get_var(@ncid, @varid)[*argv]
|
|
601
|
+
when CA_REG_SELECT
|
|
602
|
+
# A boolean mask covers the whole variable.
|
|
410
603
|
nc_get_var(@ncid, @varid)[*argv]
|
|
411
604
|
else
|
|
412
605
|
raise ArgumentError, "unsupported index for reading #{@name}"
|
|
413
606
|
end
|
|
414
607
|
end
|
|
415
608
|
|
|
609
|
+
# Index arrays per axis (`v[CA_INT([0, 7]), nil, 2..5]`): read the
|
|
610
|
+
# block that holds what they pick, and pick from it, rather than the
|
|
611
|
+
# whole variable. nil when an axis takes a form not handled here
|
|
612
|
+
# (the caller then reads the whole variable).
|
|
613
|
+
# @api private
|
|
614
|
+
private def read_grid(argv)
|
|
615
|
+
return nil unless argv.size == @shape.size
|
|
616
|
+
start, count, local = [], [], []
|
|
617
|
+
argv.each_with_index do |idx, axis|
|
|
618
|
+
n = @shape[axis]
|
|
619
|
+
case idx
|
|
620
|
+
when nil
|
|
621
|
+
start << 0; count << n; local << nil
|
|
622
|
+
when CArray
|
|
623
|
+
return nil unless INTEGER_TYPES.include?(idx.data_type)
|
|
624
|
+
i = idx.to_ca
|
|
625
|
+
i = i + (i.lt(0) * n) if i.lt(0).any # negative from the end
|
|
626
|
+
lo, hi = i.min, i.max
|
|
627
|
+
return nil unless lo.is_a?(Integer) && lo >= 0 && hi < n
|
|
628
|
+
start << lo; count << hi - lo + 1; local << i - lo
|
|
629
|
+
when Range
|
|
630
|
+
b = CArray.scan_index([n], [idx])
|
|
631
|
+
return nil unless b.type == CA_REG_BLOCK
|
|
632
|
+
first, len, step = b.index[0]
|
|
633
|
+
return nil unless step == 1
|
|
634
|
+
start << first; count << len; local << (0...len)
|
|
635
|
+
else
|
|
636
|
+
return nil
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
return nil if count.include?(0)
|
|
640
|
+
nc_get_vara(@ncid, @varid, start, count)[*local]
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# A flat array of addresses (`v[CA_INT([1, 7, -1])]`) picking a few
|
|
644
|
+
# elements: read those one by one. nil for many of them.
|
|
645
|
+
# @api private
|
|
646
|
+
MAPPING_POINT_LIMIT = 1024
|
|
647
|
+
private def read_mapping(addrs)
|
|
648
|
+
return nil unless addrs.is_a?(CArray) && INTEGER_TYPES.include?(addrs.data_type)
|
|
649
|
+
return nil if addrs.elements > MAPPING_POINT_LIMIT || data_type.nil?
|
|
650
|
+
total = @shape.inject(1, :*)
|
|
651
|
+
out = CArray.new(data_type, addrs.shape)
|
|
652
|
+
addrs.to_a.flatten.each_with_index do |a, k|
|
|
653
|
+
a += total if a < 0
|
|
654
|
+
raise IndexError, "index #{a} is out of range for #{@name}" unless (0...total).cover?(a)
|
|
655
|
+
out[k] = get_var1(addr_to_index(a))
|
|
656
|
+
end
|
|
657
|
+
out
|
|
658
|
+
end
|
|
659
|
+
|
|
416
660
|
# One element. An NC_CHAR element travels as a 1-char String in the
|
|
417
661
|
# netcdf gem's var1 calls; here it is the byte, as it is in an array
|
|
418
662
|
# read (NC_CHAR maps to :uint8).
|
|
419
663
|
# @api private
|
|
420
664
|
private def get_var1(index)
|
|
665
|
+
if @nc_type == NC_STRING
|
|
666
|
+
raise NotImplementedError,
|
|
667
|
+
"no CArray type for an NC_STRING variable; read it with " \
|
|
668
|
+
"NetCDF.nc_get_var, which returns Strings"
|
|
669
|
+
end
|
|
421
670
|
v = nc_get_var1(@ncid, @varid, index)
|
|
422
671
|
@nc_type == NC_CHAR && v.is_a?(String) ? (v.getbyte(0) || 0) : v
|
|
423
672
|
end
|
|
@@ -439,6 +688,23 @@ module CA
|
|
|
439
688
|
"got #{shape.inspect}"
|
|
440
689
|
end
|
|
441
690
|
|
|
691
|
+
# A block write takes a CArray shaped like the block, as CArray's own
|
|
692
|
+
# block assignment does (size-1 axes aside); otherwise the cells
|
|
693
|
+
# would land in the wrong places, or some be dropped.
|
|
694
|
+
# @api private
|
|
695
|
+
private def check_block_write_shape(ca, kept)
|
|
696
|
+
return if ca.shape.reject { |n| n == 1 } == kept.reject { |n| n == 1 }
|
|
697
|
+
raise ArgumentError,
|
|
698
|
+
"shape mismatch writing #{@name}: block #{kept.inspect} <- #{ca.shape.inspect}"
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# @api private
|
|
702
|
+
private def check_write_elements(ca, n)
|
|
703
|
+
return if ca.elements == n
|
|
704
|
+
raise ArgumentError,
|
|
705
|
+
"elements mismatch writing #{@name}: #{n} <- #{ca.elements}"
|
|
706
|
+
end
|
|
707
|
+
|
|
442
708
|
# -> [start, count, stride, kept_shape, reversed]; kept_shape drops
|
|
443
709
|
# point axes. libnetcdf takes only a positive stride, so a reversed
|
|
444
710
|
# range (`(9..0).step(-1)`) becomes the same cells walked forward,
|
|
@@ -488,6 +754,7 @@ module CA
|
|
|
488
754
|
# effect.
|
|
489
755
|
# @api private
|
|
490
756
|
private def refresh_shape
|
|
757
|
+
@group.file.ensure_open
|
|
491
758
|
@shape = @dimids.map { |d| nc_inq_dimlen(@ncid, d) }
|
|
492
759
|
end
|
|
493
760
|
|
|
@@ -503,7 +770,19 @@ module CA
|
|
|
503
770
|
# meaning there, so it is refused instead of landing near 2**31.
|
|
504
771
|
# @api private
|
|
505
772
|
private def scan_index_for_write(argv)
|
|
506
|
-
|
|
773
|
+
info = CArray.scan_index(@shape, argv)
|
|
774
|
+
# A range scanned against the current length is clipped to it, so
|
|
775
|
+
# `v[0..1] = ...` on an empty unlimited axis would write nothing.
|
|
776
|
+
# An index that scans the same whatever the unlimited length is
|
|
777
|
+
# means what it says; take it when it reaches further.
|
|
778
|
+
if @unlimited.any?
|
|
779
|
+
wide = CArray.scan_index(shape_for_write(INT_MAX_SCAN), argv) rescue nil
|
|
780
|
+
wider = CArray.scan_index(shape_for_write(INT_MAX_SCAN - 1), argv) rescue nil
|
|
781
|
+
if wide && wide == wider && wide != info
|
|
782
|
+
return [wide, shape_for_write(INT_MAX_SCAN)]
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
[info, @shape]
|
|
507
786
|
rescue IndexError
|
|
508
787
|
raise unless @unlimited.any?
|
|
509
788
|
shape = shape_for_write(INT_MAX_SCAN)
|
|
@@ -530,7 +809,8 @@ module CA
|
|
|
530
809
|
atts = {}
|
|
531
810
|
natts.times do |i|
|
|
532
811
|
name = nc_inq_attname(@ncid, @varid, i)
|
|
533
|
-
|
|
812
|
+
value = readable_att(@ncid, @varid, name, @name)
|
|
813
|
+
atts[name] = deep_freeze(value) unless value.equal?(UNREADABLE_ATT)
|
|
534
814
|
end
|
|
535
815
|
atts.freeze
|
|
536
816
|
end
|