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/group.rb
CHANGED
|
@@ -14,6 +14,9 @@ module CA
|
|
|
14
14
|
class Group
|
|
15
15
|
|
|
16
16
|
include CA::NetCDF
|
|
17
|
+
# The low-level calls are this class's helpers, not its interface.
|
|
18
|
+
private :nc_get_var, :nc_get_vara, :nc_get_vars, :nc_get_varm,
|
|
19
|
+
:nc_put_var, :nc_put_vara, :nc_put_vars, :nc_put_varm
|
|
17
20
|
|
|
18
21
|
# @param ncid [Integer] group id
|
|
19
22
|
# @param file [File] root file (for codec + mode + close)
|
|
@@ -28,6 +31,22 @@ module CA
|
|
|
28
31
|
load_metadata
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# Read this group's dimensions, variables, subgroups and attributes
|
|
35
|
+
# again from the file. They are read once, when the file is opened,
|
|
36
|
+
# and kept up to date by this API's own `def_*` calls; a change made
|
|
37
|
+
# through the low-level API (`nc_put_att`, `nc_rename_var`,
|
|
38
|
+
# `nc_def_var` on `ncid`) is seen only after a reload. Var, Dim and
|
|
39
|
+
# Group objects taken before it describe the old state; take new
|
|
40
|
+
# ones after it.
|
|
41
|
+
# @return [self]
|
|
42
|
+
def reload
|
|
43
|
+
@file.ensure_open
|
|
44
|
+
@dims, @vars, @groups = {}, {}, {}
|
|
45
|
+
@atts_cache = nil
|
|
46
|
+
load_metadata
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
31
50
|
# @!attribute [r] ncid
|
|
32
51
|
# @return [Integer]
|
|
33
52
|
# @!attribute [r] file
|
|
@@ -36,13 +55,13 @@ module CA
|
|
|
36
55
|
# @return [Group, nil]
|
|
37
56
|
attr_reader :ncid, :file, :parent
|
|
38
57
|
|
|
39
|
-
# @return [String] `""` for the root group, `"myGroup"` for a named
|
|
58
|
+
# @return [String] `"/"` for the root group, `"myGroup"` for a named
|
|
40
59
|
# group
|
|
41
|
-
def name = nc_inq_grpname(@ncid)
|
|
60
|
+
def name = (@file.ensure_open; nc_inq_grpname(@ncid))
|
|
42
61
|
|
|
43
62
|
# @return [String] `"/"` for root, `"/model/ocean"` for a nested
|
|
44
63
|
# group
|
|
45
|
-
def path = nc_inq_grpname_full(@ncid)
|
|
64
|
+
def path = (@file.ensure_open; nc_inq_grpname_full(@ncid))
|
|
46
65
|
|
|
47
66
|
# @return [Boolean]
|
|
48
67
|
def root? = @parent.nil?
|
|
@@ -50,11 +69,12 @@ module CA
|
|
|
50
69
|
# @!group Introspection (this group only)
|
|
51
70
|
|
|
52
71
|
# @return [Hash{String => Dim}]
|
|
53
|
-
|
|
72
|
+
# (a frozen copy: the group's own table is changed only by def_*)
|
|
73
|
+
def dims = @dims.dup.freeze
|
|
54
74
|
# @return [Hash{String => Var}]
|
|
55
|
-
def vars = @vars
|
|
75
|
+
def vars = @vars.dup.freeze
|
|
56
76
|
# @return [Hash{String => Group}]
|
|
57
|
-
def groups = @groups
|
|
77
|
+
def groups = @groups.dup.freeze
|
|
58
78
|
# @param name [#to_s]
|
|
59
79
|
# @return [Dim, nil]
|
|
60
80
|
def dim(name) = @dims[name.to_s]
|
|
@@ -74,10 +94,11 @@ module CA
|
|
|
74
94
|
# Group-scoped global attributes (each group has its own set in
|
|
75
95
|
# netcdf-4). Cached; invalidated by our own {#def_att}. Other paths
|
|
76
96
|
# that mutate attributes (`rename_att` / `del_att` / a direct
|
|
77
|
-
# `nc_put_att`) aren't exposed by
|
|
97
|
+
# `nc_put_att`) aren't exposed by the high-level API, so we don't have to watch
|
|
78
98
|
# for them.
|
|
79
99
|
# @return [Hash{String => Object}] frozen
|
|
80
100
|
def atts
|
|
101
|
+
@file.ensure_open
|
|
81
102
|
@atts_cache ||= read_group_attributes
|
|
82
103
|
end
|
|
83
104
|
|
|
@@ -129,8 +150,8 @@ module CA
|
|
|
129
150
|
n = name.to_s
|
|
130
151
|
len = length == :unlimited ? NC_UNLIMITED : Integer(length)
|
|
131
152
|
@file.ensure_define_mode
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
register_dim(Dim.new(self, nc_def_dim(@ncid, n, len),
|
|
154
|
+
unlimited: length == :unlimited))
|
|
134
155
|
end
|
|
135
156
|
|
|
136
157
|
# NetCDF-4 per-variable storage / filter options that {#def_var}
|
|
@@ -138,9 +159,16 @@ module CA
|
|
|
138
159
|
# variable attribute). See {#apply_var_options} for accepted values.
|
|
139
160
|
# @return [Array<Symbol>]
|
|
140
161
|
VAR_OPTION_KEYS = %i[
|
|
141
|
-
chunks deflate shuffle fletcher32 endian no_fill
|
|
162
|
+
chunks deflate shuffle zstandard bzip2 szip blosc fletcher32 endian no_fill
|
|
142
163
|
].freeze
|
|
143
164
|
|
|
165
|
+
# The compressors among {VAR_OPTION_KEYS}, and with shuffle, the
|
|
166
|
+
# options {#def_var} takes from one source as a whole.
|
|
167
|
+
# @api private
|
|
168
|
+
COMPRESSION_KEYS = %i[deflate zstandard bzip2 szip blosc].freeze
|
|
169
|
+
# @api private
|
|
170
|
+
COMPRESSION_GROUP = (COMPRESSION_KEYS + %i[shuffle]).freeze
|
|
171
|
+
|
|
144
172
|
# Define a variable in this group.
|
|
145
173
|
#
|
|
146
174
|
# @example Positional
|
|
@@ -165,7 +193,10 @@ module CA
|
|
|
165
193
|
# Kwargs are variable attributes, plus the reserved storage / filter
|
|
166
194
|
# options (see {VAR_OPTION_KEYS}). Overriding attributes merge into
|
|
167
195
|
# the template's; a kwarg keyed the same as a template attribute
|
|
168
|
-
# wins.
|
|
196
|
+
# wins. Between two NetCDF-4 files the template's storage settings
|
|
197
|
+
# ({Var#storage_options}) are copied too, under any given here and
|
|
198
|
+
# over the file-wide defaults; its chunk sizes are copied only when
|
|
199
|
+
# the new variable's dimensions have the template's lengths.
|
|
169
200
|
#
|
|
170
201
|
# @param name [#to_s] variable name
|
|
171
202
|
# @param type [Symbol, String, Integer, nil] on-disk type; required
|
|
@@ -179,6 +210,13 @@ module CA
|
|
|
179
210
|
def def_var(name, type = nil, dims = nil, template: nil, **atts)
|
|
180
211
|
n = name.to_s
|
|
181
212
|
|
|
213
|
+
# Split the reserved storage / filter kwargs off first, before a
|
|
214
|
+
# template's attributes are merged in under String keys.
|
|
215
|
+
given_opts = {}
|
|
216
|
+
VAR_OPTION_KEYS.each do |k|
|
|
217
|
+
given_opts[k] = atts.delete(k) if atts.key?(k)
|
|
218
|
+
end
|
|
219
|
+
|
|
182
220
|
# Accept `dims:` as a keyword too — a natural spelling when template:
|
|
183
221
|
# is in use and the positional slot would otherwise force `nil` fillers.
|
|
184
222
|
if atts.key?(:dims)
|
|
@@ -204,11 +242,14 @@ module CA
|
|
|
204
242
|
raise ArgumentError,
|
|
205
243
|
"def_var template: expected Var / Group / File, got #{template.class}"
|
|
206
244
|
end
|
|
245
|
+
template_storage = (src.file.netcdf4? && @file.netcdf4?) ? src.storage_options : {}
|
|
207
246
|
type ||= src.nc_type
|
|
208
247
|
dims ||= src.dims.map(&:name)
|
|
209
|
-
# template
|
|
210
|
-
#
|
|
211
|
-
|
|
248
|
+
# The template's attributes are copied as stored, keeping their
|
|
249
|
+
# types (below); the explicit ones (Symbol / String keys) replace
|
|
250
|
+
# any of the same name and are written by value.
|
|
251
|
+
atts = atts.transform_keys(&:to_s)
|
|
252
|
+
copied_atts = src.atts.keys - atts.keys
|
|
212
253
|
end
|
|
213
254
|
|
|
214
255
|
raise ArgumentError,
|
|
@@ -217,28 +258,119 @@ module CA
|
|
|
217
258
|
|
|
218
259
|
# Accept a name (String / Symbol) or a Dim object. Names are resolved
|
|
219
260
|
# by walking up the group tree for netcdf-4 dim inheritance.
|
|
220
|
-
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
|
|
261
|
+
# A Dim is taken as it is only when it belongs to this file and is in
|
|
262
|
+
# scope here; one from another file or a sibling group is looked up
|
|
263
|
+
# by its name, since its id means something else here.
|
|
264
|
+
dim_objects = Array(dims).map do |d|
|
|
265
|
+
if d.is_a?(Dim) && d.group.file.equal?(@file) && dim_by_id(d.to_i)
|
|
266
|
+
d
|
|
267
|
+
else
|
|
268
|
+
resolve_dim(d.is_a?(Dim) ? d.name : d.to_s)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
dimids = dim_objects.map(&:to_i)
|
|
272
|
+
# The template's chunk sizes fit only its own shape: keep them when
|
|
273
|
+
# every dimension has the template's length (or is unlimited in
|
|
274
|
+
# both), and leave chunking to libnetcdf otherwise.
|
|
275
|
+
if template_storage&.key?(:chunks) && ! same_extent?(src.dims, dim_objects)
|
|
276
|
+
template_storage.delete(:chunks)
|
|
277
|
+
end
|
|
278
|
+
# Per-var kwargs win over the template's storage, which wins over
|
|
279
|
+
# the file-wide defaults (nil / false / 0 are all "explicit"; they
|
|
280
|
+
# override). Compression is taken whole from the first of these
|
|
281
|
+
# that names any compressor, so `zstandard: 3` over a deflated
|
|
282
|
+
# template replaces deflate rather than stacking on it.
|
|
283
|
+
template_storage ||= {}
|
|
224
284
|
file_defaults = @file.var_defaults
|
|
285
|
+
chosen = [given_opts, template_storage, file_defaults].find do |o|
|
|
286
|
+
COMPRESSION_KEYS.any? { |k| o.key?(k) }
|
|
287
|
+
end
|
|
288
|
+
unless chosen.nil? || chosen.equal?(template_storage)
|
|
289
|
+
template_storage = template_storage.reject { |k, _| COMPRESSION_GROUP.include?(k) }
|
|
290
|
+
end
|
|
291
|
+
unless chosen.nil? || chosen.equal?(file_defaults)
|
|
292
|
+
file_defaults = file_defaults.reject { |k, _| COMPRESSION_GROUP.include?(k) }
|
|
293
|
+
end
|
|
225
294
|
opts = {}
|
|
226
295
|
VAR_OPTION_KEYS.each do |k|
|
|
227
|
-
if
|
|
228
|
-
opts[k] =
|
|
296
|
+
if given_opts.key?(k)
|
|
297
|
+
opts[k] = given_opts[k]
|
|
298
|
+
elsif template_storage.key?(k)
|
|
299
|
+
opts[k] = template_storage[k]
|
|
229
300
|
elsif file_defaults.key?(k)
|
|
230
301
|
opts[k] = file_defaults[k]
|
|
231
302
|
end
|
|
232
303
|
end
|
|
233
304
|
@file.ensure_define_mode
|
|
234
|
-
nct
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
305
|
+
nct = nc_type_for(type)
|
|
306
|
+
# NetCDF has no way to remove a variable, so what can be checked
|
|
307
|
+
# here is checked before nc_def_var defines it.
|
|
308
|
+
check_var_options(opts)
|
|
309
|
+
atts.each { |k, v| attribute_values(k.to_s, v) if v.is_a?(CArray) }
|
|
310
|
+
# A NaN fill (xarray writes one on every float variable) has no
|
|
311
|
+
# value in an integer type the template's type is overridden with.
|
|
312
|
+
if copied_atts && nct != src.nc_type && Var::INTEGER_TYPES.include?(NC_TO_CA[nct])
|
|
313
|
+
(copied_atts & PACKED_SPACE_ATTS).each do |k|
|
|
314
|
+
next unless Array(src.atts[k]).any? { |x| x.is_a?(Float) && !x.finite? }
|
|
315
|
+
raise ArgumentError,
|
|
316
|
+
"the template's #{k} is #{src.atts[k].inspect}, which an integer type " \
|
|
317
|
+
"cannot hold; give #{k}: for the new type"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
varid = nc_def_var(@ncid, n, nct, dimids)
|
|
321
|
+
begin
|
|
322
|
+
apply_var_options(varid, opts) unless opts.empty?
|
|
323
|
+
# A packed-space attribute takes the variable's type (CF §2.5.1),
|
|
324
|
+
# a CArray value included; another CArray keeps its own type.
|
|
325
|
+
unsigned = unsigned_flag?(atts["_Unsigned"] || atts[:_Unsigned] || (copied_atts && src.atts["_Unsigned"]))
|
|
326
|
+
atts.each { |k, v| put_var_att(@ncid, varid, k.to_s, v, nct, unsigned: unsigned) }
|
|
327
|
+
copied_atts&.each do |k|
|
|
328
|
+
# A packed-space attribute follows the variable when its type
|
|
329
|
+
# was overridden; anything else keeps the template's type.
|
|
330
|
+
if PACKED_SPACE_ATTS.include?(k) && nct != src.nc_type
|
|
331
|
+
put_var_att(@ncid, varid, k, src.atts[k], nct, unsigned: unsigned)
|
|
332
|
+
else
|
|
333
|
+
copy_att_from(src.group, src.varid, k, varid)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
ensure
|
|
337
|
+
# Registered even when a later step failed, so that the object
|
|
338
|
+
# model matches the file (the block form's abort rolls it back).
|
|
339
|
+
@vars[n] = Var.new(self, varid)
|
|
340
|
+
end
|
|
341
|
+
@vars[n]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Raise on storage options that would fail only after the variable
|
|
345
|
+
# is defined: a value of the wrong form, or a NetCDF-4 option on a
|
|
346
|
+
# classic file.
|
|
347
|
+
# @api private
|
|
348
|
+
private def check_var_options(opts)
|
|
349
|
+
c = opts[:chunks]
|
|
350
|
+
unless c.nil? || c.is_a?(Array) || %i[contiguous compact].include?(c)
|
|
351
|
+
raise ArgumentError, "chunks: expected Array / :contiguous / :compact, got #{c.inspect}"
|
|
352
|
+
end
|
|
353
|
+
e = opts[:endian]
|
|
354
|
+
unless e.nil? || %i[native little big].include?(e)
|
|
355
|
+
raise ArgumentError, "endian: expected :native / :little / :big, got #{e.inspect}"
|
|
356
|
+
end
|
|
357
|
+
szip_arguments(opts[:szip]) if opts[:szip]
|
|
358
|
+
blosc_arguments(opts[:blosc]) if opts[:blosc]
|
|
359
|
+
return if @file.netcdf4?
|
|
360
|
+
netcdf4_only = []
|
|
361
|
+
netcdf4_only << :chunks if c
|
|
362
|
+
netcdf4_only << :deflate if opts[:deflate].to_i.positive?
|
|
363
|
+
netcdf4_only << :shuffle if opts[:shuffle]
|
|
364
|
+
netcdf4_only << :zstandard if opts[:zstandard] && opts[:zstandard].to_i != 0
|
|
365
|
+
netcdf4_only << :bzip2 if opts[:bzip2].to_i.positive?
|
|
366
|
+
netcdf4_only << :szip if opts[:szip]
|
|
367
|
+
netcdf4_only << :blosc if opts[:blosc]
|
|
368
|
+
netcdf4_only << :fletcher32 if opts[:fletcher32]
|
|
369
|
+
netcdf4_only << :endian if e
|
|
370
|
+
return if netcdf4_only.empty?
|
|
371
|
+
raise ArgumentError,
|
|
372
|
+
"#{netcdf4_only.map { |k| "#{k}:" }.join(', ')} need a NetCDF-4 file " \
|
|
373
|
+
"(create it with netcdf4: true)"
|
|
242
374
|
end
|
|
243
375
|
|
|
244
376
|
# Apply NetCDF-4 storage / filter options on a freshly defined
|
|
@@ -248,12 +380,21 @@ module CA
|
|
|
248
380
|
# chunks: :contiguous # NC_CONTIGUOUS
|
|
249
381
|
# chunks: :compact # NC_COMPACT (small vars only)
|
|
250
382
|
# deflate: 4 # compression level 1..9 (0 / nil = off)
|
|
251
|
-
# shuffle: true # shuffle filter, usually paired with
|
|
383
|
+
# shuffle: true # shuffle filter, usually paired with a compressor
|
|
384
|
+
# zstandard: 3 # zstandard level (0 / nil = off; libnetcdf >= 4.9)
|
|
385
|
+
# bzip2: 9 # bzip2 level 1..9 (0 / nil = off; libnetcdf >= 4.9)
|
|
386
|
+
# szip: { coding: :nn, pixels_per_block: 32 } # coding :nn / :ec
|
|
387
|
+
# blosc: { compressor: :lz4, level: 5, blocksize: 0, shuffle: :byte }
|
|
388
|
+
# # libnetcdf >= 4.9; compressor :lz / :lz4 /
|
|
389
|
+
# # :lz4hc / :snappy / :zlib / :zstd,
|
|
390
|
+
# # shuffle :none / :byte / :bit
|
|
252
391
|
# fletcher32: true # per-chunk fletcher32 checksum
|
|
253
392
|
# endian: :little / :big / :native
|
|
254
393
|
# no_fill: true # disable pre-fill (perf)
|
|
255
394
|
#
|
|
256
|
-
# These all require NetCDF-4
|
|
395
|
+
# These all require NetCDF-4, except `no_fill:` and an off
|
|
396
|
+
# `deflate:` (0 / nil, without `shuffle:`), which is a new variable's
|
|
397
|
+
# default and so is not sent; libnetcdf raises on a NetCDF-3 file
|
|
257
398
|
# and the raise propagates.
|
|
258
399
|
# @api private
|
|
259
400
|
private def apply_var_options(varid, opts)
|
|
@@ -266,13 +407,33 @@ module CA
|
|
|
266
407
|
end
|
|
267
408
|
end
|
|
268
409
|
if opts.key?(:deflate) || opts.key?(:shuffle)
|
|
410
|
+
# The netcdf gem reads these flags as Ruby truth values, so they
|
|
411
|
+
# go as true / false (a 0 would read as true).
|
|
269
412
|
level = (opts[:deflate] || 0).to_i
|
|
270
|
-
on = level.positive?
|
|
271
|
-
shuffle = opts[:shuffle] ?
|
|
272
|
-
|
|
413
|
+
on = level.positive?
|
|
414
|
+
shuffle = opts[:shuffle] ? true : false
|
|
415
|
+
if on || shuffle
|
|
416
|
+
nc_def_var_deflate(@ncid, varid, shuffle, on, level)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
# The other compressors, after shuffle so that it runs first. A
|
|
420
|
+
# filter libnetcdf lacks raises NC_ENOFILTER from the netcdf gem.
|
|
421
|
+
# zstandard also takes negative ("fast") levels; only 0 / nil / false
|
|
422
|
+
# is off, and libnetcdf checks the range.
|
|
423
|
+
if (level = opts[:zstandard]) && level.to_i != 0
|
|
424
|
+
nc_def_var_zstandard(@ncid, varid, level.to_i)
|
|
425
|
+
end
|
|
426
|
+
if opts[:bzip2].to_i.positive?
|
|
427
|
+
nc_def_var_bzip2(@ncid, varid, opts[:bzip2].to_i)
|
|
428
|
+
end
|
|
429
|
+
if (s = opts[:szip])
|
|
430
|
+
nc_def_var_szip(@ncid, varid, *szip_arguments(s))
|
|
431
|
+
end
|
|
432
|
+
if (b = opts[:blosc])
|
|
433
|
+
nc_def_var_blosc(@ncid, varid, *blosc_arguments(b))
|
|
273
434
|
end
|
|
274
435
|
if opts[:fletcher32]
|
|
275
|
-
nc_def_var_fletcher32(@ncid, varid,
|
|
436
|
+
nc_def_var_fletcher32(@ncid, varid, true)
|
|
276
437
|
end
|
|
277
438
|
if (e = opts[:endian])
|
|
278
439
|
endian = case e
|
|
@@ -284,8 +445,81 @@ module CA
|
|
|
284
445
|
nc_def_var_endian(@ncid, varid, endian)
|
|
285
446
|
end
|
|
286
447
|
if opts.key?(:no_fill)
|
|
287
|
-
nc_def_var_fill(@ncid, varid, opts[:no_fill] ?
|
|
448
|
+
nc_def_var_fill(@ncid, varid, opts[:no_fill] ? true : false, nil)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
SZIP_CODINGS = { nn: :NC_SZIP_NN, ec: :NC_SZIP_EC }.freeze
|
|
453
|
+
BLOSC_COMPRESSORS = {
|
|
454
|
+
lz: :BLOSC_LZ, lz4: :BLOSC_LZ4, lz4hc: :BLOSC_LZ4HC,
|
|
455
|
+
snappy: :BLOSC_SNAPPY, zlib: :BLOSC_ZLIB, zstd: :BLOSC_ZSTD,
|
|
456
|
+
}.freeze
|
|
457
|
+
BLOSC_SHUFFLES = { none: :BLOSC_NOSHUFFLE, byte: :BLOSC_SHUFFLE, bit: :BLOSC_BITSHUFFLE }.freeze
|
|
458
|
+
private_constant :SZIP_CODINGS, :BLOSC_COMPRESSORS, :BLOSC_SHUFFLES
|
|
459
|
+
|
|
460
|
+
# Whether two lists of Dims have the same extent: the same number,
|
|
461
|
+
# and each pair unlimited in both or of the same length.
|
|
462
|
+
# @api private
|
|
463
|
+
private def same_extent?(dims, others)
|
|
464
|
+
return false unless dims.size == others.size
|
|
465
|
+
dims.zip(others).all? do |a, b|
|
|
466
|
+
if a.unlimited? || b.unlimited?
|
|
467
|
+
a.unlimited? == b.unlimited?
|
|
468
|
+
else
|
|
469
|
+
a.length == b.length
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# `szip: { coding:, pixels_per_block: }` as the arguments of
|
|
475
|
+
# `nc_def_var_szip`.
|
|
476
|
+
# @api private
|
|
477
|
+
private def szip_arguments(opt)
|
|
478
|
+
unless opt.is_a?(Hash)
|
|
479
|
+
raise ArgumentError, "szip: expected a Hash, got #{opt.inspect}"
|
|
480
|
+
end
|
|
481
|
+
coding = SZIP_CODINGS[opt.fetch(:coding, :nn)] or
|
|
482
|
+
raise ArgumentError, "szip: coding: expected :nn / :ec, got #{opt[:coding].inspect}"
|
|
483
|
+
[::NetCDF.const_get(coding), Integer(opt.fetch(:pixels_per_block, 32))]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# `blosc: { compressor:, level:, blocksize:, shuffle: }` as the
|
|
487
|
+
# arguments of `nc_def_var_blosc`.
|
|
488
|
+
# @api private
|
|
489
|
+
private def blosc_arguments(opt)
|
|
490
|
+
unless opt.is_a?(Hash)
|
|
491
|
+
raise ArgumentError, "blosc: expected a Hash, got #{opt.inspect}"
|
|
492
|
+
end
|
|
493
|
+
compressor = BLOSC_COMPRESSORS[opt.fetch(:compressor, :lz4)] or
|
|
494
|
+
raise ArgumentError, "blosc: compressor: expected one of #{BLOSC_COMPRESSORS.keys.inspect}, got #{opt[:compressor].inspect}"
|
|
495
|
+
shuffle = BLOSC_SHUFFLES[opt.fetch(:shuffle, :byte)] or
|
|
496
|
+
raise ArgumentError, "blosc: shuffle: expected :none / :byte / :bit, got #{opt[:shuffle].inspect}"
|
|
497
|
+
[::NetCDF.const_get(compressor), Integer(opt.fetch(:level, 5)),
|
|
498
|
+
Integer(opt.fetch(:blocksize, 0)), ::NetCDF.const_get(shuffle)]
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Copy attribute `name` of `src_varid` (or `NC_GLOBAL`) in `src_group`
|
|
502
|
+
# onto `varid` here, keeping its type. When libnetcdf cannot (an
|
|
503
|
+
# `NC_STRING` attribute into a classic file), it is written by value.
|
|
504
|
+
# @api private
|
|
505
|
+
private def copy_att_from(src_group, src_varid, name, varid = NC_GLOBAL)
|
|
506
|
+
@file.ensure_define_mode
|
|
507
|
+
begin
|
|
508
|
+
nc_copy_att(src_group.ncid, src_varid, name, @ncid, varid)
|
|
509
|
+
rescue ::NetCDF::Error
|
|
510
|
+
value = nc_get_att(src_group.ncid, src_varid, name)
|
|
511
|
+
if value.is_a?(Array) && value.all?(String)
|
|
512
|
+
raise ArgumentError,
|
|
513
|
+
"attribute #{name.inspect} holds several strings (NC_STRING), " \
|
|
514
|
+
"which a classic-format file cannot store; give it a value of your own"
|
|
515
|
+
end
|
|
516
|
+
if varid == NC_GLOBAL
|
|
517
|
+
nc_put_att(@ncid, NC_GLOBAL, name, value)
|
|
518
|
+
else
|
|
519
|
+
put_var_att(@ncid, varid, name, value, nc_inq_vartype(@ncid, varid))
|
|
520
|
+
end
|
|
288
521
|
end
|
|
522
|
+
@atts_cache = nil if varid == NC_GLOBAL
|
|
289
523
|
end
|
|
290
524
|
|
|
291
525
|
# Define a group-level (global) attribute. A CArray value keeps its
|
|
@@ -296,7 +530,7 @@ module CA
|
|
|
296
530
|
def def_att(name, value)
|
|
297
531
|
@file.ensure_define_mode
|
|
298
532
|
if value.is_a?(CArray)
|
|
299
|
-
put_att_typed(@ncid, NC_GLOBAL, name.to_s,
|
|
533
|
+
put_att_typed(@ncid, NC_GLOBAL, name.to_s, attribute_values(name.to_s, value), TYPE_TO_NC[value.data_type])
|
|
300
534
|
else
|
|
301
535
|
nc_put_att(@ncid, NC_GLOBAL, name.to_s, value)
|
|
302
536
|
end
|
|
@@ -338,7 +572,7 @@ module CA
|
|
|
338
572
|
# copy_var("lat", from: src, units: "deg") # override / add attributes
|
|
339
573
|
#
|
|
340
574
|
# The data copy is bit-identical: raw on-disk values are read via
|
|
341
|
-
# {Var#raw} (no codec) and written via
|
|
575
|
+
# {Var#raw} (no codec) and written via the low-level API (no codec). Users who
|
|
342
576
|
# need a codec-mediated copy should call `def_var(template:)` and
|
|
343
577
|
# write `dst[name] = src[name].to_ca` themselves.
|
|
344
578
|
#
|
|
@@ -358,7 +592,17 @@ module CA
|
|
|
358
592
|
dst = @vars[dst_name]
|
|
359
593
|
ensure_data_mode
|
|
360
594
|
# Bit-identical raw copy (skip both source's decode and dst's encode).
|
|
361
|
-
if src.
|
|
595
|
+
if src.nc_type == NC_STRING
|
|
596
|
+
# Strings have no CArray type; the netcdf gem moves them as Ruby
|
|
597
|
+
# Strings, nested by shape.
|
|
598
|
+
strings = ::NetCDF.nc_get_var(src.group.ncid, src.varid)
|
|
599
|
+
shape = src.shape
|
|
600
|
+
if shape.empty?
|
|
601
|
+
::NetCDF.nc_put_var(@ncid, dst.varid, strings)
|
|
602
|
+
elsif shape.inject(1, :*) > 0
|
|
603
|
+
::NetCDF.nc_put_vara(@ncid, dst.varid, shape.map { 0 }, shape, Array(strings).flatten)
|
|
604
|
+
end
|
|
605
|
+
elsif src.scalar?
|
|
362
606
|
buf = CArray.new(src.data_type, [1])
|
|
363
607
|
buf[0] = src.raw
|
|
364
608
|
nc_put_var(@ncid, dst.varid, buf)
|
|
@@ -393,13 +637,17 @@ module CA
|
|
|
393
637
|
def ensure_define_mode = @file.ensure_define_mode
|
|
394
638
|
# @return [void]
|
|
395
639
|
def ensure_data_mode = @file.ensure_data_mode
|
|
640
|
+
# Run a block in define or data mode ({File#__transaction__}).
|
|
396
641
|
# @api private
|
|
397
642
|
def __transaction__(**opts, &block) = @file.__transaction__(**opts, &block)
|
|
398
643
|
|
|
399
644
|
# @!endgroup
|
|
400
645
|
|
|
401
646
|
# @return [String]
|
|
402
|
-
def inspect
|
|
647
|
+
def inspect
|
|
648
|
+
return "#<#{self.class} (closed)>" if @file.closed?
|
|
649
|
+
"#<#{self.class} #{path.inspect} vars=#{@vars.keys} groups=#{@groups.keys}>"
|
|
650
|
+
end
|
|
403
651
|
|
|
404
652
|
# Resolve a dimension name by walking up the group tree — netcdf-4
|
|
405
653
|
# dimension inheritance.
|
|
@@ -423,15 +671,23 @@ module CA
|
|
|
423
671
|
# @return [Dim, nil]
|
|
424
672
|
# @api private
|
|
425
673
|
def dim_by_id(dimid)
|
|
674
|
+
# Dim ids are unique in a file; the file keeps them in one table,
|
|
675
|
+
# and the dim is found here when it is in this group's scope.
|
|
676
|
+
d = @file.dim_index[dimid] or return nil
|
|
426
677
|
g = self
|
|
427
678
|
while g
|
|
428
|
-
d
|
|
429
|
-
return d if d
|
|
679
|
+
return d if g.equal?(d.group)
|
|
430
680
|
g = g.parent
|
|
431
681
|
end
|
|
432
682
|
nil
|
|
433
683
|
end
|
|
434
684
|
|
|
685
|
+
# @api private
|
|
686
|
+
private def register_dim(dim)
|
|
687
|
+
@file.dim_index[dim.to_i] = dim
|
|
688
|
+
@dims[dim.name] = dim
|
|
689
|
+
end
|
|
690
|
+
|
|
435
691
|
# ---- template / copy source resolution -----------------------------
|
|
436
692
|
# Common: accept the object directly (Dim / Var) or a name + `from:`
|
|
437
693
|
# group; raise cleanly on the invalid combinations.
|
|
@@ -469,6 +725,7 @@ module CA
|
|
|
469
725
|
# Resolve a "/absolute" or "relative/nested" path down to a Var.
|
|
470
726
|
# @api private
|
|
471
727
|
private def resolve_path(str)
|
|
728
|
+
raise KeyError, "path #{str.inspect} names no variable (it ends in /)" if str.end_with?("/")
|
|
472
729
|
parts = str.split("/").reject(&:empty?)
|
|
473
730
|
raise KeyError, "empty path" if parts.empty?
|
|
474
731
|
leaf = parts.pop
|
|
@@ -495,7 +752,7 @@ module CA
|
|
|
495
752
|
# fallback path is needed; a real error should propagate rather than
|
|
496
753
|
# be silently swallowed (any rescue here used to also catch
|
|
497
754
|
# permission errors and pretend "netcdf-3 layout" — misleading).
|
|
498
|
-
nc_inq_dimids(@ncid, false).each { |d|
|
|
755
|
+
nc_inq_dimids(@ncid, false).each { |d| register_dim(Dim.new(self, d)) }
|
|
499
756
|
nc_inq_varids(@ncid).each { |v| vr = Var.new(self, v); @vars[vr.name] = vr }
|
|
500
757
|
nc_inq_grps(@ncid).each do |gid|
|
|
501
758
|
g = Group.new(gid, @file, self)
|
|
@@ -508,7 +765,8 @@ module CA
|
|
|
508
765
|
atts = {}
|
|
509
766
|
nc_inq_natts(@ncid).times do |i|
|
|
510
767
|
name = nc_inq_attname(@ncid, NC_GLOBAL, i)
|
|
511
|
-
|
|
768
|
+
value = readable_att(@ncid, NC_GLOBAL, name, path)
|
|
769
|
+
atts[name] = deep_freeze(value) unless value.equal?(UNREADABLE_ATT)
|
|
512
770
|
end
|
|
513
771
|
atts.freeze
|
|
514
772
|
end
|