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,517 @@
|
|
|
1
|
+
module CA
|
|
2
|
+
module NetCDF
|
|
3
|
+
# A NetCDF-4 group. The root group is a {File} (a subclass of this).
|
|
4
|
+
#
|
|
5
|
+
# nc.groups # => {String => Group}
|
|
6
|
+
# nc.group("model") # => Group (subgroup by name in this group)
|
|
7
|
+
# nc.def_group("model") # => Group (create + return)
|
|
8
|
+
# nc["temp"] # => Var (in this group)
|
|
9
|
+
# nc["/model/temp"] # => Var (full path from root)
|
|
10
|
+
#
|
|
11
|
+
# Dims defined in an ancestor group are visible to a `def_var` / {Var}
|
|
12
|
+
# in a descendant (netcdf-4 dimension inheritance), so `def_var`
|
|
13
|
+
# resolves a dim name by walking up the group tree.
|
|
14
|
+
class Group
|
|
15
|
+
|
|
16
|
+
include CA::NetCDF
|
|
17
|
+
|
|
18
|
+
# @param ncid [Integer] group id
|
|
19
|
+
# @param file [File] root file (for codec + mode + close)
|
|
20
|
+
# @param parent [Group, nil] parent group (nil for root)
|
|
21
|
+
def initialize(ncid, file, parent = nil)
|
|
22
|
+
@ncid = ncid
|
|
23
|
+
@file = file
|
|
24
|
+
@parent = parent
|
|
25
|
+
@dims = {}
|
|
26
|
+
@vars = {}
|
|
27
|
+
@groups = {}
|
|
28
|
+
load_metadata
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @!attribute [r] ncid
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
# @!attribute [r] file
|
|
34
|
+
# @return [File] root file
|
|
35
|
+
# @!attribute [r] parent
|
|
36
|
+
# @return [Group, nil]
|
|
37
|
+
attr_reader :ncid, :file, :parent
|
|
38
|
+
|
|
39
|
+
# @return [String] `""` for the root group, `"myGroup"` for a named
|
|
40
|
+
# group
|
|
41
|
+
def name = nc_inq_grpname(@ncid)
|
|
42
|
+
|
|
43
|
+
# @return [String] `"/"` for root, `"/model/ocean"` for a nested
|
|
44
|
+
# group
|
|
45
|
+
def path = nc_inq_grpname_full(@ncid)
|
|
46
|
+
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def root? = @parent.nil?
|
|
49
|
+
|
|
50
|
+
# @!group Introspection (this group only)
|
|
51
|
+
|
|
52
|
+
# @return [Hash{String => Dim}]
|
|
53
|
+
def dims = @dims
|
|
54
|
+
# @return [Hash{String => Var}]
|
|
55
|
+
def vars = @vars
|
|
56
|
+
# @return [Hash{String => Group}]
|
|
57
|
+
def groups = @groups
|
|
58
|
+
# @param name [#to_s]
|
|
59
|
+
# @return [Dim, nil]
|
|
60
|
+
def dim(name) = @dims[name.to_s]
|
|
61
|
+
# @param name [#to_s]
|
|
62
|
+
# @return [Var, nil]
|
|
63
|
+
def var(name) = @vars[name.to_s]
|
|
64
|
+
# @param name [#to_s]
|
|
65
|
+
# @return [Group, nil]
|
|
66
|
+
def group(name) = @groups[name.to_s]
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def dim?(name) = @dims.key?(name.to_s)
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def var?(name) = @vars.key?(name.to_s)
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def group?(name) = @groups.key?(name.to_s)
|
|
73
|
+
|
|
74
|
+
# Group-scoped global attributes (each group has its own set in
|
|
75
|
+
# netcdf-4). Cached; invalidated by our own {#def_att}. Other paths
|
|
76
|
+
# that mutate attributes (`rename_att` / `del_att` / a direct
|
|
77
|
+
# `nc_put_att`) aren't exposed by layer 2, so we don't have to watch
|
|
78
|
+
# for them.
|
|
79
|
+
# @return [Hash{String => Object}] frozen
|
|
80
|
+
def atts
|
|
81
|
+
@atts_cache ||= read_group_attributes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param name [#to_s]
|
|
85
|
+
# @return [Object, nil]
|
|
86
|
+
def att(name) = atts[name.to_s]
|
|
87
|
+
|
|
88
|
+
# @!endgroup
|
|
89
|
+
|
|
90
|
+
# @!group Access
|
|
91
|
+
|
|
92
|
+
# Look up a variable by name or path.
|
|
93
|
+
#
|
|
94
|
+
# nc["temp"] # Var in this group
|
|
95
|
+
# nc["/foo/bar/x"] # Var at that path from the root
|
|
96
|
+
# nc["foo/x"] # Var at that path relative to this group
|
|
97
|
+
#
|
|
98
|
+
# @param name_or_path [String, Symbol]
|
|
99
|
+
# @return [Var]
|
|
100
|
+
# @raise [KeyError] if the variable / any path segment doesn't exist
|
|
101
|
+
def [](name_or_path)
|
|
102
|
+
s = name_or_path.to_s
|
|
103
|
+
return resolve_path(s) if s.include?("/")
|
|
104
|
+
@vars[s] or raise KeyError, "no variable #{s.inspect} in group #{path}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Convenience combined variable-lookup and write.
|
|
108
|
+
#
|
|
109
|
+
# nc[name] = ca # whole-variable write
|
|
110
|
+
# nc[name, *index] = ca # hyperslab write
|
|
111
|
+
# nc["/path/name", ...] = ca # full-path write
|
|
112
|
+
#
|
|
113
|
+
# @return [Object] the value written
|
|
114
|
+
def []=(*argv)
|
|
115
|
+
value = argv.pop
|
|
116
|
+
name = argv.shift
|
|
117
|
+
self[name][*argv] = value
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @!endgroup
|
|
121
|
+
|
|
122
|
+
# @!group Definition (scoped to this group)
|
|
123
|
+
|
|
124
|
+
# Define a dimension.
|
|
125
|
+
# @param name [#to_s]
|
|
126
|
+
# @param length [Integer, :unlimited]
|
|
127
|
+
# @return [Dim]
|
|
128
|
+
def def_dim(name, length)
|
|
129
|
+
n = name.to_s
|
|
130
|
+
len = length == :unlimited ? NC_UNLIMITED : Integer(length)
|
|
131
|
+
@file.ensure_define_mode
|
|
132
|
+
@dims[n] = Dim.new(self, nc_def_dim(@ncid, n, len),
|
|
133
|
+
unlimited: length == :unlimited)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# NetCDF-4 per-variable storage / filter options that {#def_var}
|
|
137
|
+
# recognizes in its keyword arguments (any other kwarg becomes a
|
|
138
|
+
# variable attribute). See {#apply_var_options} for accepted values.
|
|
139
|
+
# @return [Array<Symbol>]
|
|
140
|
+
VAR_OPTION_KEYS = %i[
|
|
141
|
+
chunks deflate shuffle fletcher32 endian no_fill
|
|
142
|
+
].freeze
|
|
143
|
+
|
|
144
|
+
# Define a variable in this group.
|
|
145
|
+
#
|
|
146
|
+
# @example Positional
|
|
147
|
+
# def_var("temp", :float, ["time","lat","lon"], units: "K")
|
|
148
|
+
# @example Copy definition
|
|
149
|
+
# def_var("temp", template: src["temp"])
|
|
150
|
+
# @example Look up name in a template group
|
|
151
|
+
# def_var("temp", template: src)
|
|
152
|
+
# @example Rename on copy
|
|
153
|
+
# def_var("temp2", template: src["temp"])
|
|
154
|
+
# @example Override type
|
|
155
|
+
# def_var("temp", :double, template: src["temp"])
|
|
156
|
+
# @example Override / add attributes
|
|
157
|
+
# def_var("temp", template: src, units: "F")
|
|
158
|
+
#
|
|
159
|
+
# When `template:` is a {Var}, its definition (type, dims, atts) is
|
|
160
|
+
# used; positional `type` / `dims` and kwargs are treated as
|
|
161
|
+
# overrides. When `template:` is a {File} / {Group}, its variable of
|
|
162
|
+
# the same name (first positional arg) is looked up and used as the
|
|
163
|
+
# template — the rename form must use the {Var} version.
|
|
164
|
+
#
|
|
165
|
+
# Kwargs are variable attributes, plus the reserved storage / filter
|
|
166
|
+
# options (see {VAR_OPTION_KEYS}). Overriding attributes merge into
|
|
167
|
+
# the template's; a kwarg keyed the same as a template attribute
|
|
168
|
+
# wins.
|
|
169
|
+
#
|
|
170
|
+
# @param name [#to_s] variable name
|
|
171
|
+
# @param type [Symbol, String, Integer, nil] on-disk type; required
|
|
172
|
+
# unless a `template:` is given
|
|
173
|
+
# @param dims [Array<String, Symbol, Dim>, nil] dimensions
|
|
174
|
+
# @param template [Var, Group, File, nil] source of a copied
|
|
175
|
+
# definition
|
|
176
|
+
# @param atts [Hash] variable attributes and/or storage options
|
|
177
|
+
# (see {VAR_OPTION_KEYS})
|
|
178
|
+
# @return [Var]
|
|
179
|
+
def def_var(name, type = nil, dims = nil, template: nil, **atts)
|
|
180
|
+
n = name.to_s
|
|
181
|
+
|
|
182
|
+
# Accept `dims:` as a keyword too — a natural spelling when template:
|
|
183
|
+
# is in use and the positional slot would otherwise force `nil` fillers.
|
|
184
|
+
if atts.key?(:dims)
|
|
185
|
+
raise ArgumentError,
|
|
186
|
+
"def_var: `dims` given both positionally and as a keyword" if dims
|
|
187
|
+
dims = atts.delete(:dims)
|
|
188
|
+
end
|
|
189
|
+
# Symmetric for type: `type:` as keyword. Same collision rule.
|
|
190
|
+
if atts.key?(:type)
|
|
191
|
+
raise ArgumentError,
|
|
192
|
+
"def_var: `type` given both positionally and as a keyword" if type
|
|
193
|
+
type = atts.delete(:type)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if template
|
|
197
|
+
src = case template
|
|
198
|
+
when Var then template
|
|
199
|
+
when Group then template.var(n) or
|
|
200
|
+
raise KeyError,
|
|
201
|
+
"def_var(#{n.inspect}, template: ...): no variable " \
|
|
202
|
+
"#{n.inspect} in template group #{template.path}"
|
|
203
|
+
else
|
|
204
|
+
raise ArgumentError,
|
|
205
|
+
"def_var template: expected Var / Group / File, got #{template.class}"
|
|
206
|
+
end
|
|
207
|
+
type ||= src.nc_type
|
|
208
|
+
dims ||= src.dims.map(&:name)
|
|
209
|
+
# template.atts (String keys, frozen) merged under any explicit
|
|
210
|
+
# atts (Symbol / String keys). Explicit wins on collision.
|
|
211
|
+
atts = src.atts.merge(atts.transform_keys(&:to_s))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
raise ArgumentError,
|
|
215
|
+
"def_var: `type` is required (or pass a template:)" if type.nil?
|
|
216
|
+
dims ||= []
|
|
217
|
+
|
|
218
|
+
# Accept a name (String / Symbol) or a Dim object. Names are resolved
|
|
219
|
+
# by walking up the group tree for netcdf-4 dim inheritance.
|
|
220
|
+
dimids = Array(dims).map { |d| d.is_a?(Dim) ? d.to_i : resolve_dim(d.to_s).to_i }
|
|
221
|
+
# Split reserved storage/filter kwargs off before treating the rest
|
|
222
|
+
# as variable attributes. Per-var kwargs win over the file-wide
|
|
223
|
+
# defaults (nil / false / 0 are all "explicit"; they override).
|
|
224
|
+
file_defaults = @file.var_defaults
|
|
225
|
+
opts = {}
|
|
226
|
+
VAR_OPTION_KEYS.each do |k|
|
|
227
|
+
if atts.key?(k)
|
|
228
|
+
opts[k] = atts.delete(k)
|
|
229
|
+
elsif file_defaults.key?(k)
|
|
230
|
+
opts[k] = file_defaults[k]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
@file.ensure_define_mode
|
|
234
|
+
nct = nc_type_for(type)
|
|
235
|
+
varid = nc_def_var(@ncid, n, nct, dimids)
|
|
236
|
+
apply_var_options(varid, opts) unless opts.empty?
|
|
237
|
+
# A CArray value keeps its own on-disk type (so a schema loader's
|
|
238
|
+
# `_FillValue: {short: -32768}` on a float variable stays short);
|
|
239
|
+
# a packed-space attribute takes the variable's type (CF §2.5.1).
|
|
240
|
+
atts.each { |k, v| put_var_att(@ncid, varid, k.to_s, v, nct) }
|
|
241
|
+
@vars[n] = Var.new(self, varid)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Apply NetCDF-4 storage / filter options on a freshly defined
|
|
245
|
+
# variable. Kwargs on {#def_var}:
|
|
246
|
+
#
|
|
247
|
+
# chunks: [1, 73, 144] # NC_CHUNKED with explicit sizes
|
|
248
|
+
# chunks: :contiguous # NC_CONTIGUOUS
|
|
249
|
+
# chunks: :compact # NC_COMPACT (small vars only)
|
|
250
|
+
# deflate: 4 # compression level 1..9 (0 / nil = off)
|
|
251
|
+
# shuffle: true # shuffle filter, usually paired with deflate
|
|
252
|
+
# fletcher32: true # per-chunk fletcher32 checksum
|
|
253
|
+
# endian: :little / :big / :native
|
|
254
|
+
# no_fill: true # disable pre-fill (perf)
|
|
255
|
+
#
|
|
256
|
+
# These all require NetCDF-4; libnetcdf raises on a NetCDF-3 file
|
|
257
|
+
# and the raise propagates.
|
|
258
|
+
# @api private
|
|
259
|
+
private def apply_var_options(varid, opts)
|
|
260
|
+
if (c = opts[:chunks])
|
|
261
|
+
case c
|
|
262
|
+
when :contiguous then nc_def_var_chunking(@ncid, varid, NC_CONTIGUOUS, [])
|
|
263
|
+
when :compact then nc_def_var_chunking(@ncid, varid, NC_COMPACT, [])
|
|
264
|
+
when Array then nc_def_var_chunking(@ncid, varid, NC_CHUNKED, c)
|
|
265
|
+
else raise ArgumentError, "chunks: expected Array / :contiguous / :compact, got #{c.inspect}"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
if opts.key?(:deflate) || opts.key?(:shuffle)
|
|
269
|
+
level = (opts[:deflate] || 0).to_i
|
|
270
|
+
on = level.positive? ? 1 : 0
|
|
271
|
+
shuffle = opts[:shuffle] ? 1 : 0
|
|
272
|
+
nc_def_var_deflate(@ncid, varid, shuffle, on, level)
|
|
273
|
+
end
|
|
274
|
+
if opts[:fletcher32]
|
|
275
|
+
nc_def_var_fletcher32(@ncid, varid, 1)
|
|
276
|
+
end
|
|
277
|
+
if (e = opts[:endian])
|
|
278
|
+
endian = case e
|
|
279
|
+
when :native then NC_ENDIAN_NATIVE
|
|
280
|
+
when :little then NC_ENDIAN_LITTLE
|
|
281
|
+
when :big then NC_ENDIAN_BIG
|
|
282
|
+
else raise ArgumentError, "endian: expected :native / :little / :big, got #{e.inspect}"
|
|
283
|
+
end
|
|
284
|
+
nc_def_var_endian(@ncid, varid, endian)
|
|
285
|
+
end
|
|
286
|
+
if opts.key?(:no_fill)
|
|
287
|
+
nc_def_var_fill(@ncid, varid, opts[:no_fill] ? 1 : 0, nil)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Define a group-level (global) attribute. A CArray value keeps its
|
|
292
|
+
# own type, as it does on a variable.
|
|
293
|
+
# @param name [#to_s]
|
|
294
|
+
# @param value [Object] Ruby value or CArray
|
|
295
|
+
# @return [Object] the value written
|
|
296
|
+
def def_att(name, value)
|
|
297
|
+
@file.ensure_define_mode
|
|
298
|
+
if value.is_a?(CArray)
|
|
299
|
+
put_att_typed(@ncid, NC_GLOBAL, name.to_s, value.to_a, TYPE_TO_NC[value.data_type])
|
|
300
|
+
else
|
|
301
|
+
nc_put_att(@ncid, NC_GLOBAL, name.to_s, value)
|
|
302
|
+
end
|
|
303
|
+
@atts_cache = nil # invalidate — new attribute is now visible
|
|
304
|
+
value
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Copy a dimension into this group.
|
|
308
|
+
#
|
|
309
|
+
# @example
|
|
310
|
+
# copy_dim(src.dim("time")) # same name
|
|
311
|
+
# copy_dim(src.dim("time"), name: "t") # rename
|
|
312
|
+
# copy_dim("time", from: src) # look up by name in src
|
|
313
|
+
#
|
|
314
|
+
# An unlimited source becomes an unlimited destination dim.
|
|
315
|
+
# Coordinate values (the same-named variable, if any) are NOT
|
|
316
|
+
# copied — use {#copy_var} separately, or `File.create(template:,
|
|
317
|
+
# vars: [...])`.
|
|
318
|
+
#
|
|
319
|
+
# @param dim_or_name [Dim, String, Symbol]
|
|
320
|
+
# @param from [Group, File, nil] required when `dim_or_name` is a
|
|
321
|
+
# name
|
|
322
|
+
# @param name [String, Symbol, nil] destination name (defaults to
|
|
323
|
+
# the source's)
|
|
324
|
+
# @return [Dim]
|
|
325
|
+
def copy_dim(dim_or_name, from: nil, name: nil)
|
|
326
|
+
src = resolve_dim_source(dim_or_name, from)
|
|
327
|
+
dst_name = (name || src.name).to_s
|
|
328
|
+
len = src.unlimited? ? :unlimited : src.length
|
|
329
|
+
def_dim(dst_name, len)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Copy a variable's definition AND its data into this group.
|
|
333
|
+
#
|
|
334
|
+
# @example
|
|
335
|
+
# copy_var(src["lat"]) # same name, values included
|
|
336
|
+
# copy_var(src["lat"], name: "latitude") # rename
|
|
337
|
+
# copy_var("lat", from: src) # look up by name in src
|
|
338
|
+
# copy_var("lat", from: src, units: "deg") # override / add attributes
|
|
339
|
+
#
|
|
340
|
+
# The data copy is bit-identical: raw on-disk values are read via
|
|
341
|
+
# {Var#raw} (no codec) and written via layer 1 (no codec). Users who
|
|
342
|
+
# need a codec-mediated copy should call `def_var(template:)` and
|
|
343
|
+
# write `dst[name] = src[name].to_ca` themselves.
|
|
344
|
+
#
|
|
345
|
+
# Extra kwargs are forwarded to {#def_var}(template:), so per-var
|
|
346
|
+
# storage options (`deflate:`, `chunks:`, …) and attribute
|
|
347
|
+
# overrides both work.
|
|
348
|
+
#
|
|
349
|
+
# @param var_or_name [Var, String, Symbol]
|
|
350
|
+
# @param from [Group, File, nil]
|
|
351
|
+
# @param name [String, Symbol, nil]
|
|
352
|
+
# @param overrides [Hash] attribute / storage overrides
|
|
353
|
+
# @return [Var]
|
|
354
|
+
def copy_var(var_or_name, from: nil, name: nil, **overrides)
|
|
355
|
+
src = resolve_var_source(var_or_name, from)
|
|
356
|
+
dst_name = (name || src.name).to_s
|
|
357
|
+
def_var(dst_name, template: src, **overrides)
|
|
358
|
+
dst = @vars[dst_name]
|
|
359
|
+
ensure_data_mode
|
|
360
|
+
# Bit-identical raw copy (skip both source's decode and dst's encode).
|
|
361
|
+
if src.scalar?
|
|
362
|
+
buf = CArray.new(src.data_type, [1])
|
|
363
|
+
buf[0] = src.raw
|
|
364
|
+
nc_put_var(@ncid, dst.varid, buf)
|
|
365
|
+
else
|
|
366
|
+
# nc_put_var writes exactly the variable's current shape. For an
|
|
367
|
+
# unlimited-dim var, the destination shape starts at length 0 in the
|
|
368
|
+
# newly-defined var, so nc_put_var would silently write nothing;
|
|
369
|
+
# nc_put_vara with explicit count grows the unlimited dim to fit.
|
|
370
|
+
ca = src.raw
|
|
371
|
+
nc_put_vara(@ncid, dst.varid, ca.shape.map { 0 }, ca.shape, ca)
|
|
372
|
+
end
|
|
373
|
+
dst
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Define a subgroup.
|
|
377
|
+
# @param name [#to_s]
|
|
378
|
+
# @return [Group]
|
|
379
|
+
def def_group(name)
|
|
380
|
+
n = name.to_s
|
|
381
|
+
@file.ensure_define_mode
|
|
382
|
+
child = nc_def_grp(@ncid, n)
|
|
383
|
+
@groups[n] = Group.new(child, @file, self)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# @!endgroup
|
|
387
|
+
|
|
388
|
+
# @!group Forwarding to the root {File} (codec + mode management)
|
|
389
|
+
|
|
390
|
+
# @return [Object] the codec used for this file
|
|
391
|
+
def codec = @file.codec
|
|
392
|
+
# @return [void]
|
|
393
|
+
def ensure_define_mode = @file.ensure_define_mode
|
|
394
|
+
# @return [void]
|
|
395
|
+
def ensure_data_mode = @file.ensure_data_mode
|
|
396
|
+
# @api private
|
|
397
|
+
def __transaction__(**opts, &block) = @file.__transaction__(**opts, &block)
|
|
398
|
+
|
|
399
|
+
# @!endgroup
|
|
400
|
+
|
|
401
|
+
# @return [String]
|
|
402
|
+
def inspect = "#<#{self.class} #{path.inspect} vars=#{@vars.keys} groups=#{@groups.keys}>"
|
|
403
|
+
|
|
404
|
+
# Resolve a dimension name by walking up the group tree — netcdf-4
|
|
405
|
+
# dimension inheritance.
|
|
406
|
+
# @param name [#to_s]
|
|
407
|
+
# @return [Dim]
|
|
408
|
+
# @raise [KeyError] if not found in the ancestor chain
|
|
409
|
+
def resolve_dim(name)
|
|
410
|
+
g = self
|
|
411
|
+
while g
|
|
412
|
+
d = g.dim(name)
|
|
413
|
+
return d if d
|
|
414
|
+
g = g.parent
|
|
415
|
+
end
|
|
416
|
+
raise KeyError, "no dimension #{name.inspect} in scope of #{path}"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The {Dim} with this id, in this group or an ancestor (netcdf-4
|
|
420
|
+
# dimids are unique across a file, so the id is unambiguous where a
|
|
421
|
+
# name may be shadowed).
|
|
422
|
+
# @param dimid [Integer]
|
|
423
|
+
# @return [Dim, nil]
|
|
424
|
+
# @api private
|
|
425
|
+
def dim_by_id(dimid)
|
|
426
|
+
g = self
|
|
427
|
+
while g
|
|
428
|
+
d = g.dims.each_value.find { |x| x.to_i == dimid }
|
|
429
|
+
return d if d
|
|
430
|
+
g = g.parent
|
|
431
|
+
end
|
|
432
|
+
nil
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# ---- template / copy source resolution -----------------------------
|
|
436
|
+
# Common: accept the object directly (Dim / Var) or a name + `from:`
|
|
437
|
+
# group; raise cleanly on the invalid combinations.
|
|
438
|
+
|
|
439
|
+
# @api private
|
|
440
|
+
private def resolve_dim_source(dim_or_name, from)
|
|
441
|
+
if dim_or_name.is_a?(Dim)
|
|
442
|
+
raise ArgumentError,
|
|
443
|
+
"copy_dim: pass either a Dim or (name, from:), not both" if from
|
|
444
|
+
dim_or_name
|
|
445
|
+
else
|
|
446
|
+
raise ArgumentError,
|
|
447
|
+
"copy_dim(name, from:): `from:` is required" unless from
|
|
448
|
+
from.dim(dim_or_name.to_s) or
|
|
449
|
+
raise KeyError,
|
|
450
|
+
"copy_dim: no dimension #{dim_or_name.inspect} in #{from.path}"
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# @api private
|
|
455
|
+
private def resolve_var_source(var_or_name, from)
|
|
456
|
+
if var_or_name.is_a?(Var)
|
|
457
|
+
raise ArgumentError,
|
|
458
|
+
"copy_var: pass either a Var or (name, from:), not both" if from
|
|
459
|
+
var_or_name
|
|
460
|
+
else
|
|
461
|
+
raise ArgumentError,
|
|
462
|
+
"copy_var(name, from:): `from:` is required" unless from
|
|
463
|
+
from.var(var_or_name.to_s) or
|
|
464
|
+
raise KeyError,
|
|
465
|
+
"copy_var: no variable #{var_or_name.inspect} in #{from.path}"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Resolve a "/absolute" or "relative/nested" path down to a Var.
|
|
470
|
+
# @api private
|
|
471
|
+
private def resolve_path(str)
|
|
472
|
+
parts = str.split("/").reject(&:empty?)
|
|
473
|
+
raise KeyError, "empty path" if parts.empty?
|
|
474
|
+
leaf = parts.pop
|
|
475
|
+
start = str.start_with?("/") ? @file : self
|
|
476
|
+
g = start
|
|
477
|
+
parts.each do |part|
|
|
478
|
+
nxt = g.group(part) or
|
|
479
|
+
raise KeyError, "no group #{part.inspect} in #{g.path}"
|
|
480
|
+
g = nxt
|
|
481
|
+
end
|
|
482
|
+
g[leaf]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# @api private
|
|
486
|
+
private def load_metadata
|
|
487
|
+
# Dimensions defined IN THIS GROUP (not inherited). nc_inq_dimids
|
|
488
|
+
# with include_parents=0 gives exactly this group's dims; falling
|
|
489
|
+
# back to nc_inq_ndims/nc_inq_dimname for the root of a netcdf-3
|
|
490
|
+
# file (no groups, no dimids call needed but works anyway).
|
|
491
|
+
# nc_inq_dimids's include_parents arg is Ruby-truthy in the netcdf gem
|
|
492
|
+
# (RTEST), so pass `false` — plain `0` is truthy and would include
|
|
493
|
+
# parent-group dims. We deliberately want THIS group's dims only.
|
|
494
|
+
# libnetcdf's nc_inq_dimids/varids/grps work on netcdf-3 too, so no
|
|
495
|
+
# fallback path is needed; a real error should propagate rather than
|
|
496
|
+
# be silently swallowed (any rescue here used to also catch
|
|
497
|
+
# permission errors and pretend "netcdf-3 layout" — misleading).
|
|
498
|
+
nc_inq_dimids(@ncid, false).each { |d| dm = Dim.new(self, d); @dims[dm.name] = dm }
|
|
499
|
+
nc_inq_varids(@ncid).each { |v| vr = Var.new(self, v); @vars[vr.name] = vr }
|
|
500
|
+
nc_inq_grps(@ncid).each do |gid|
|
|
501
|
+
g = Group.new(gid, @file, self)
|
|
502
|
+
@groups[g.name] = g
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# @api private
|
|
507
|
+
private def read_group_attributes
|
|
508
|
+
atts = {}
|
|
509
|
+
nc_inq_natts(@ncid).times do |i|
|
|
510
|
+
name = nc_inq_attname(@ncid, NC_GLOBAL, i)
|
|
511
|
+
atts[name] = nc_get_att(@ncid, NC_GLOBAL, name)
|
|
512
|
+
end
|
|
513
|
+
atts.freeze
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|