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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f3e60226c628ade3bb1c17f553e5cd2ab593370eb89d00246f7139ed2e6b44f
4
- data.tar.gz: 68fba916f7cacf44bb63af2e01374886401d66f52c870a67ab0c949a348da6b2
3
+ metadata.gz: 71bb04b44cd8c06925d970fb1e69ebdea46b7320aeec1e3c542aca3668a66cfc
4
+ data.tar.gz: 864e70e40ec87e90200e74ca97590e7449808dd8e5d012ce64aac8805182733f
5
5
  SHA512:
6
- metadata.gz: d03a0d7d42e11f09363a21686cb45fde2e28b641af8848ce7d87f68465f3184cb34db1aeb3d6454fa18ebee978bb2e028dcf8dda737e7e2b65d2ad21cf743f88
7
- data.tar.gz: 50073e499670da6e882f8ce3a43d84a32dfd1cb53b496cbd6bfb795ee18867cc1f4124bc9c438d3c4aeef55a95148f011c1182ec3bd08a015a1fdc21fade7687
6
+ metadata.gz: 99b7b5824c461197bbdb4b7161d59728d095976c207964d34208d560a7a14ef4c9a432ef8a76ee974ad97095fe4b89e611dd5b9e262cf25b20f734c30eb0d737
7
+ data.tar.gz: d9ef82a43aaacdb8d9e0b83786d3f0d222f4738feb3fc2b3b785cc01cbcf7117ae83c5f51372831c3b80e4bfb503ab84e231917568adc9c2551133d09fab65f9
data/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ Releases are recorded here from 2.0.0, the first release of the rewrite.
4
+ There is no separate NEWS file: this is where to look for what changed
5
+ between the version you have and a newer one. The 1.x series was a
6
+ different codebase and is not covered.
7
+
8
+ <!-- Newest first, at both levels: a new release section goes above the
9
+ ones below it, and a new entry goes directly under its own release
10
+ heading -- not at the end of the section. The kind of change is
11
+ carried by the `- Fix:` / `- Change:` / `- New:` that opens the
12
+ entry; there are no per-kind subheadings.
13
+
14
+ A section is written newest-first while the release is open, and
15
+ sorted into New, Change, Fix when it closes -- in the same commit
16
+ that drops `(unreleased)`. Within Change, the ones that ask the
17
+ reader to change code come first.
18
+
19
+ An entry says three things and stops: what changed, what to do about
20
+ it (the migration, the replacement, the condition under which nothing
21
+ changes), and what is excluded. It does not say how the code was
22
+ broken or name the internals that were fixed -- those belong in the
23
+ commit message. Two to six lines.
24
+
25
+ It is written for someone using the library, not someone working on
26
+ it: with no NEWS file, this is what a reader consults before
27
+ upgrading.
28
+
29
+ The version here is this gem's own and is neither CArray's nor the
30
+ netcdf gem's. Which of those a release needs is said in the gemspec,
31
+ and an entry says so only when the answer changes. -->
32
+
33
+ ## 2.0.0
34
+
35
+ A ground-up rewrite. Nothing from 1.x is carried over, so read this as a
36
+ new library that happens to keep the name rather than as an upgrade.
37
+
38
+ - Change: the gem is pure Ruby and no longer builds a C extension of its
39
+ own. libnetcdf is reached through the [netcdf](https://github.com/himotoyoshi/netcdf)
40
+ gem, and CArray 3.0 hands it memory through the MemoryView protocol, so
41
+ nothing is copied on the way in or out. Installing it therefore needs
42
+ libnetcdf present for that gem to build against, not for this one.
43
+
44
+ - Change: NetCDF-4 files are read and written, where 1.x was NetCDF-3
45
+ only. That brings groups, `NC_STRING`, per-variable chunking and
46
+ compression, and the unlimited dimension in its NetCDF-4 form.
47
+
48
+ - Change: everything lives in `CA::NetCDF`, and `::NetCDF` — the thin
49
+ binding — is included rather than modified, so a future `Numo::NetCDF`
50
+ can sit beside it. Code written against 1.x's names does not run here.
51
+
52
+ - New: layer 1, `CA::NetCDF`'s own `nc_*` functions. They are the
53
+ netcdf gem's API with the array I/O overridden to take and return
54
+ CArrays; definition, inquiry, attribute, and group calls pass through
55
+ verbatim. Usable as module functions or by `include CA::NetCDF`.
56
+
57
+ - New: layer 2, the object-oriented surface — `File`, `Group`, `Var`,
58
+ `Dim`. `File.open` / `File.create`, `nc[:temp]` to read a variable,
59
+ `nc[:temp, 0..9] = data` to write a hyperslab, lazy hyperslab reads,
60
+ file-wide variable option defaults, and atomic writes through a
61
+ visible `.tmp` name.
62
+
63
+ - New: pluggable conventions. CF and COARDS codecs decode `_FillValue`
64
+ into a CArray mask and apply `scale_factor` / `add_offset`, CF time
65
+ becomes `CATime`, and `valid_range` masking is available opt-in.
66
+ `Identity` is the codec that does none of it.
67
+
68
+ - New: `File.create` takes a template, so a file can be generated with
69
+ another file's dimensions, attributes, and variable definitions —
70
+ copied, overridden, or skipped per item.
71
+
72
+ - New: `ncheader` and `ncvar` on the command line. `ncheader` prints a
73
+ file's header as CDL, JSON, or YAML; `ncvar` streams a variable, or a
74
+ hyperslab of one, as raw native-endian bytes, text, or JSON.
75
+
76
+ - New: Zarr stores open and create through `File.open` / `File.create`
77
+ given a URL such as `"file:///data/t.zarr#mode=zarr,file"`, when
78
+ libnetcdf was built with NCZarr. `atomic:` and `clobber: false` /
79
+ `:skip` take a file path only and raise `ArgumentError` with a URL.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,75 @@
1
+ # Contributing to carray-netcdf
2
+
3
+ Thanks for looking. The short version: an issue is easier to act on here than a pull request.
4
+
5
+ ## What to send
6
+
7
+ | What you have | How to send it |
8
+ |---|---|
9
+ | **A bug report or a feature request** | Open an issue. |
10
+ | **A small, self-contained bug fix** | Open a pull request. |
11
+ | **Anything larger** | Open an issue first. |
12
+
13
+ carray-netcdf was built the way the README describes: designed and reviewed by me, implemented in collaboration with AI coding tools. That has a consequence worth saying plainly, because it is not visible from outside. Code here gets rewritten as a matter of course — mine included. A patch that touches the internals is likely to be reimplemented rather than merged, and not because anything is wrong with it. That is simply how work moves through this repository.
14
+
15
+ So a description travels further than a diff. An issue saying what you were trying to do, what happened, and what you expected can be acted on directly.
16
+
17
+ A small, self-contained bug fix is the exception. Send it as a pull request.
18
+
19
+ ## Which repository a bug belongs to
20
+
21
+ Three gems are stacked here, and the layer decides where the issue goes:
22
+
23
+ - **carray-netcdf** — anything about `CA::NetCDF`: the codecs, the mask, `File` / `Group` / `Var` / `Dim`, how a hyperslab is addressed.
24
+ - **[netcdf](https://github.com/himotoyoshi/netcdf)** — a bare `nc_*` call that misbehaves, a libnetcdf feature that is missing, a build failure against libnetcdf.
25
+ - **[carray](https://github.com/himotoyoshi/carray)** — arithmetic, slicing, or masks going wrong on an array once it is in hand.
26
+
27
+ When you are not sure, open it here. Being told which of three repos to use is not a reasonable thing to ask of a reporter.
28
+
29
+ ## Opening an issue
30
+
31
+ Issues in English or Japanese are both fine.
32
+
33
+ For a bug, include:
34
+
35
+ - carray-netcdf version (`CA::NetCDF::VERSION`), CArray version (`CArray::VERSION`), netcdf gem version (`NetCDF::VERSION`), libnetcdf version (`NetCDF.nc_inq_libvers`), Ruby version, and OS
36
+ - The smallest script that shows the problem
37
+ - What you expected and what you got
38
+
39
+ And, where they apply, the things most bugs here turn on:
40
+
41
+ - the **codec** in force. Unless you named one, it was chosen from the file's `Conventions` attribute, so two files can read differently through the same code. A value wrong by exactly `scale_factor` is a codec question rather than an I/O one — say what `codec: :identity` gives for the same read
42
+ - whether elements came back **masked**, and what the variable's `_FillValue` / `valid_range` attributes are
43
+ - the variable's **NetCDF type** against the CArray type you got, when the complaint is about values rather than shape
44
+ - whether the dimension involved is **unlimited**
45
+ - the file itself, or what `ncdump -h` says about it. A file that one writer produced and another cannot read is the common case, and the header settles it
46
+
47
+ ## Sending a fix
48
+
49
+ Ruby 3.1 or later, CArray and the netcdf gem in the ranges the gemspec declares. Sibling checkouts of `carray` and `netcdf` beside this one are picked up automatically when they are there; otherwise the installed gems are used.
50
+
51
+ ```sh
52
+ rake spec # the whole suite (also the default task)
53
+ rake dependency_check # do the gems in use fall inside the gemspec's ranges
54
+ ```
55
+
56
+ `rake spec` must report no failures.
57
+
58
+ The tests are hand written, one file per area, and a test for your fix goes beside the ones it belongs with. They write real NetCDF files into a temporary directory and read them back: a change is considered tested when the values survive the round trip, not when the call returns without raising.
59
+
60
+ Then, in the same pull request:
61
+
62
+ - If you changed what a public method does or accepts, update its documentation comment in `lib/` and the page in `docs/` that covers it. [docs/08_cheatsheet.md](docs/08_cheatsheet.md) lists the entry points and goes stale first.
63
+ - Add an entry to [CHANGELOG.md](CHANGELOG.md), at the top of the unreleased section, opening with `- Fix:`. Write it for someone who hit the bug: what behaves differently now, not how the code was wrong.
64
+
65
+ Keep the commit message to a subject line and a line or two of context. Anything a user needs to know belongs in the changelog entry rather than in the commit.
66
+
67
+ ## Working on the code
68
+
69
+ `docs/` is the guide and is written to be read in order. The code is one file per class under `lib/carray/netcdf/`, with layer 1 — the `nc_*` functions that speak CArray — in `lib/carray/netcdf.rb` itself.
70
+
71
+ The line worth holding while working here: layer 1 is raw values only. No convention decoding, no mask handling; reads come back unmasked and writing a masked CArray raises. Everything about `_FillValue`, `scale_factor`, and time belongs to the codecs in layer 2.
72
+
73
+ ## License
74
+
75
+ By submitting a pull request, you agree that your contribution is licensed under the MIT License, the same terms as the rest of the project.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 himotoyoshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,3 +1,109 @@
1
- This directory contains the plugin library used as the interface to NetCDF
2
- from Ruby/CArray.
1
+ # carray-netcdf
3
2
 
3
+ A NetCDF interface for [CArray](https://github.com/himotoyoshi/carray), built on the [netcdf](https://github.com/himotoyoshi/netcdf) gem.
4
+
5
+ Data is exchanged as CArray at every level: reads return a CArray and writes accept one. Both go through Ruby's MemoryView protocol, so libnetcdf reads and writes the array's own memory, with no intermediate representation of the data.
6
+
7
+ Two layers do this, both in the `CA::NetCDF` module. Layer 1 turns the netcdf gem's MemoryView buffers into CArray, keeping the `nc_*` functions as they are. Layer 2 builds the object-oriented interface on top: `File`, `Group`, `Var`, `Dim` and the codecs. Both are pure Ruby, on CArray 3.0; every libnetcdf call goes through the netcdf gem.
8
+
9
+ Layer 2 gives you data as the file's conventions define it. For a file declaring CF or COARDS, values are decoded on the way out: `_FillValue` becomes a mask, `scale_factor` and `add_offset` are applied, and time coordinates become CATime. Variables are indexed like CArrays, and only the elements you index are read — `nc["temp"][0..9, nil, nil]` reads ten days, not the whole variable.
10
+
11
+ ## Status
12
+
13
+ 2.0 still moves: its own surface is not settled yet, so behaviour can change between releases.
14
+
15
+ ## Features
16
+
17
+ * **Object-oriented access to NetCDF data.** A file, a group, a variable and a dimension are objects: `File.open`, `nc["temp"]`, `var.dims`, `var.atts`, and a block form that closes the file for you.
18
+ * **CArray in and out.** Reads return a CArray and writes accept one. A variable is indexed the way a CArray is, so only the slab you ask for is read, and libnetcdf works on the array's own memory rather than a copy of it.
19
+ * **Conventions are decoded — CF, COARDS, or none.** Under CF or COARDS, `_FillValue` becomes a mask, `scale_factor` and `add_offset` are applied, and a time axis becomes CATime. `codec: :identity` gives you the stored values instead, and a codec of your own can take their place.
20
+ * **NetCDF-4 groups.** Variables are addressed by path (`nc["/obs/temp"]`), dimensions are inherited from the groups above, and the file's codec applies inside them.
21
+ * **Writing is a schema and an assignment.** `def_dim`, `def_var`, `def_att`, then `nc["temp"] = ca`. Attributes that CF requires to match the variable's type are written in that type, so other tools read the file as intended.
22
+ * **Atomic writes.** `File.create(atomic: true)` writes to a temporary file in the same directory and renames it into place only when the block completes, so a failed run leaves no half-written file behind.
23
+
24
+ ## Install
25
+
26
+ libnetcdf and its headers have to be present first, because the netcdf gem underneath is built against them:
27
+
28
+ ```
29
+ brew install netcdf # macOS
30
+ apt install libnetcdf-dev # Debian / Ubuntu
31
+ dnf install netcdf-devel # Fedora / RHEL
32
+ ```
33
+
34
+ Then:
35
+
36
+ ```
37
+ gem install carray-netcdf
38
+ ```
39
+
40
+ Or add it to your `Gemfile`:
41
+
42
+ ```ruby
43
+ gem "carray-netcdf"
44
+ ```
45
+
46
+ Requires:
47
+
48
+ - Ruby >= 3.1
49
+ - carray >= 3.0.1
50
+ - the netcdf gem, installed as a dependency
51
+
52
+ ## Example
53
+
54
+ ```ruby
55
+ require "carray-netcdf"
56
+
57
+ NC = CA::NetCDF
58
+
59
+ # --- write ---
60
+ NC::File.create("out.nc") do |nc|
61
+ nc.def_dim("time", :unlimited)
62
+ nc.def_dim("lat", 73)
63
+ nc.def_dim("lon", 144)
64
+
65
+ nc.def_var("temp", :float, ["time", "lat", "lon"],
66
+ units: "K", _FillValue: -999.9)
67
+
68
+ nc.def_att("Conventions", "CF-1.7")
69
+
70
+ nc["temp"] = CArray.float32(1, 73, 144) { rand * 30 + 260 }
71
+ end
72
+
73
+ # --- read ---
74
+ NC::File.open("out.nc") do |nc|
75
+ t = nc["temp"]
76
+ t.shape # => [1, 73, 144]
77
+ t.dims.map(&:name) # => ["time", "lat", "lon"]
78
+
79
+ day = t[0, nil, nil] # a 2-D CArray; only this slab is read
80
+ day.mean # CF decoding has already been applied
81
+ end
82
+ ```
83
+
84
+ ## Documentation
85
+
86
+ * [Introduction](docs/00_introduction.md) — why there are two layers, and what the gem leaves to CArray
87
+ * [Getting started](docs/01_getting_started.md) — install, then read and write a first file
88
+ * [Reading files](docs/02_reading.md) — opening a file, finding a variable, reading all or part of it
89
+ * [Writing files](docs/03_writing.md) — declaring the schema, writing data, changing a file later
90
+ * [Codecs and CF conventions](docs/04_codec.md) — what gets decoded, how a codec is chosen, writing your own
91
+ * [Groups](docs/05_groups.md) — NetCDF-4 groups, and what they inherit
92
+ * [Layer 1](docs/06_layer1.md) — the `nc_*` functions with CArray in and out
93
+ * [Cookbook](docs/07_cookbook.md) — worked solutions to real tasks
94
+ * [Cheatsheet](docs/08_cheatsheet.md) — every method and constant of the gem on one page
95
+ * [Command-line tools](docs/09_cli.md) — `ncheader` and `ncvar`, which ship with the gem
96
+
97
+ ## Contributing
98
+
99
+ Bug reports and feature requests are welcome — please open an issue.
100
+
101
+ **An issue is easier to act on here than a pull request.** [CONTRIBUTING.md](CONTRIBUTING.md) says why, and what to put in one.
102
+
103
+ ## Credits
104
+
105
+ carray-netcdf was designed and reviewed by a human developer; the implementation was produced in collaboration with AI coding tools.
106
+
107
+ ## License
108
+
109
+ MIT
data/bin/ncheader ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env ruby
2
+ # ncheader — dump a NetCDF file's header in CDL (ncdump -h style), JSON,
3
+ # or YAML. Header only: dimensions, variables, attributes, groups, and
4
+ # user-defined types — data values are not printed.
5
+ #
6
+ # $ ncheader FILE.nc # CDL (default)
7
+ # $ ncheader -f json FILE.nc
8
+ # $ ncheader -f yaml FILE.nc
9
+
10
+ require "netcdf"
11
+ require "json"
12
+ require "yaml"
13
+ require "optparse"
14
+
15
+ PRIMITIVE_TYPE_NAMES = {
16
+ NetCDF::NC_BYTE => "byte",
17
+ NetCDF::NC_CHAR => "char",
18
+ NetCDF::NC_SHORT => "short",
19
+ NetCDF::NC_INT => "int",
20
+ NetCDF::NC_FLOAT => "float",
21
+ NetCDF::NC_DOUBLE => "double",
22
+ NetCDF::NC_UBYTE => "ubyte",
23
+ NetCDF::NC_USHORT => "ushort",
24
+ NetCDF::NC_UINT => "uint",
25
+ NetCDF::NC_INT64 => "int64",
26
+ NetCDF::NC_UINT64 => "uint64",
27
+ NetCDF::NC_STRING => "string",
28
+ }
29
+
30
+ def type_name(ncid, xtype)
31
+ PRIMITIVE_TYPE_NAMES[xtype] || NetCDF.nc_inq_type(ncid, xtype).first
32
+ end
33
+
34
+ def collect_attrs(ncid, varid, natts)
35
+ natts.times.map do |i|
36
+ name = NetCDF.nc_inq_attname(ncid, varid, i)
37
+ xtype, _len = NetCDF.nc_inq_att(ncid, varid, name)
38
+ {
39
+ "name" => name,
40
+ "type" => type_name(ncid, xtype),
41
+ "value" => NetCDF.nc_get_att(ncid, varid, name),
42
+ }
43
+ end
44
+ end
45
+
46
+ def collect_user_types(ncid)
47
+ typeids = NetCDF.nc_inq_typeids(ncid)
48
+ typeids.map do |tid|
49
+ name, _size, base_type, _nfields, klass = NetCDF.nc_inq_user_type(ncid, tid)
50
+ info = { "name" => name, "typeid" => tid }
51
+ case klass
52
+ when NetCDF::NC_ENUM
53
+ _, _, _, nmembers = NetCDF.nc_inq_enum(ncid, tid)
54
+ info["kind"] = "enum"
55
+ info["base"] = type_name(ncid, base_type)
56
+ info["members"] = nmembers.times.map { |i|
57
+ mname, mvalue = NetCDF.nc_inq_enum_member(ncid, tid, i)
58
+ { "name" => mname, "value" => mvalue }
59
+ }
60
+ when NetCDF::NC_OPAQUE
61
+ _, osize = NetCDF.nc_inq_opaque(ncid, tid)
62
+ info["kind"] = "opaque"
63
+ info["size"] = osize
64
+ when NetCDF::NC_VLEN
65
+ _, _dsize, btype = NetCDF.nc_inq_vlen(ncid, tid)
66
+ info["kind"] = "vlen"
67
+ info["base"] = type_name(ncid, btype)
68
+ when NetCDF::NC_COMPOUND
69
+ info["kind"] = "compound" # field metadata not yet exposed by gem
70
+ end
71
+ info
72
+ end
73
+ end
74
+
75
+ def inspect_group(ncid)
76
+ _, _, natts, unlimid = NetCDF.nc_inq(ncid)
77
+
78
+ dims = NetCDF.nc_inq_dimids(ncid, false).map { |id|
79
+ name, len = NetCDF.nc_inq_dim(ncid, id)
80
+ { "name" => name, "length" => len, "unlimited" => (id == unlimid) }
81
+ }
82
+
83
+ vars = NetCDF.nc_inq_varids(ncid).map { |vid|
84
+ vname, xtype, _ndims, vdimids, vnatts = NetCDF.nc_inq_var(ncid, vid)
85
+ {
86
+ "name" => vname,
87
+ "type" => type_name(ncid, xtype),
88
+ "dimensions" => vdimids.map { |d| NetCDF.nc_inq_dimname(ncid, d) },
89
+ "attributes" => collect_attrs(ncid, vid, vnatts),
90
+ }
91
+ }
92
+
93
+ subgroups = NetCDF.nc_inq_grps(ncid).map { |gid|
94
+ inspect_group(gid).merge("name" => NetCDF.nc_inq_grpname(gid))
95
+ }
96
+
97
+ {
98
+ "types" => collect_user_types(ncid),
99
+ "dimensions" => dims,
100
+ "variables" => vars,
101
+ "attributes" => collect_attrs(ncid, NetCDF::NC_GLOBAL, natts),
102
+ "groups" => subgroups,
103
+ }
104
+ end
105
+
106
+ # ---- CDL formatter ----
107
+
108
+ TYPE_SUFFIX = {
109
+ "byte" => "b", "ubyte" => "UB",
110
+ "short" => "s", "ushort" => "US",
111
+ "int" => "", "uint" => "U",
112
+ "int64" => "LL", "uint64" => "ULL",
113
+ "float" => "f", "double" => "",
114
+ }
115
+
116
+ def cdl_escape(s)
117
+ s.to_s.gsub(/[\\"\n\r\t]/, "\\" => '\\\\', '"' => '\\"',
118
+ "\n" => '\\n', "\r" => '\\r', "\t" => '\\t')
119
+ end
120
+
121
+ def cdl_value(type, value)
122
+ return %("#{cdl_escape(value)}") if type == "char"
123
+ arr = Array(value)
124
+ if type == "string"
125
+ return arr.map { |s| %("#{cdl_escape(s)}") }.join(", ")
126
+ end
127
+ suffix = TYPE_SUFFIX[type] || ""
128
+ arr.map { |v|
129
+ if v.is_a?(Float)
130
+ type == "float" ? "#{v}f" : v.to_s
131
+ else
132
+ "#{v}#{suffix}"
133
+ end
134
+ }.join(", ")
135
+ end
136
+
137
+ def emit_cdl(io, group, name, indent = 0)
138
+ pre = " " * indent
139
+ head = indent.zero? ? "netcdf #{name} {" : "group: #{name} {"
140
+ io.puts "#{pre}#{head}"
141
+
142
+ unless group["types"].empty?
143
+ io.puts "#{pre}types:"
144
+ group["types"].each { |t|
145
+ case t["kind"]
146
+ when "enum"
147
+ members = t["members"].map { |m| "#{m["name"]} = #{m["value"]}" }.join(", ")
148
+ io.puts "#{pre} #{t["base"]} enum #{t["name"]} {#{members}} ;"
149
+ when "opaque"
150
+ io.puts "#{pre} opaque(#{t["size"]}) #{t["name"]} ;"
151
+ when "vlen"
152
+ io.puts "#{pre} #{t["base"]}(*) #{t["name"]} ;"
153
+ when "compound"
154
+ io.puts "#{pre} compound #{t["name"]} { /* fields not exposed */ } ;"
155
+ end
156
+ }
157
+ end
158
+
159
+ unless group["dimensions"].empty?
160
+ io.puts "#{pre}dimensions:"
161
+ group["dimensions"].each { |d|
162
+ if d["unlimited"]
163
+ io.puts "#{pre} #{d["name"]} = UNLIMITED ; // (#{d["length"]} currently)"
164
+ else
165
+ io.puts "#{pre} #{d["name"]} = #{d["length"]} ;"
166
+ end
167
+ }
168
+ end
169
+
170
+ unless group["variables"].empty?
171
+ io.puts "#{pre}variables:"
172
+ group["variables"].each { |v|
173
+ dims = v["dimensions"].empty? ? "" : "(#{v["dimensions"].join(", ")})"
174
+ io.puts "#{pre} #{v["type"]} #{v["name"]}#{dims} ;"
175
+ v["attributes"].each { |a|
176
+ io.puts "#{pre} #{v["name"]}:#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
177
+ }
178
+ }
179
+ end
180
+
181
+ unless group["attributes"].empty?
182
+ io.puts
183
+ label = indent.zero? ? "global attributes" : "group attributes"
184
+ io.puts "#{pre}// #{label}:"
185
+ group["attributes"].each { |a|
186
+ io.puts "#{pre} :#{a["name"]} = #{cdl_value(a["type"], a["value"])} ;"
187
+ }
188
+ end
189
+
190
+ group["groups"].each { |g|
191
+ io.puts
192
+ emit_cdl(io, g, g["name"], indent + 1)
193
+ }
194
+
195
+ io.puts indent.zero? ? "#{pre}}" : "#{pre}} // group #{name}"
196
+ end
197
+
198
+ # ---- Main ----
199
+
200
+ format = "cdl"
201
+ OptionParser.new { |o|
202
+ o.banner = "usage: ncheader [-f cdl|json|yaml] FILE.nc"
203
+ o.on("-f FMT", "--format FMT", %w[cdl json yaml],
204
+ "output format (default: cdl)") { |v| format = v }
205
+ }.parse!
206
+
207
+ path = ARGV[0] or abort "usage: ncheader [-f cdl|json|yaml] FILE.nc"
208
+
209
+ ncid = NetCDF.nc_open(path, NetCDF::NC_NOWRITE)
210
+ begin
211
+ tree = inspect_group(ncid).merge(
212
+ "name" => File.basename(path, ".nc"),
213
+ "format" => NetCDF.nc_inq_format(ncid),
214
+ )
215
+ ensure
216
+ NetCDF.nc_close(ncid)
217
+ end
218
+
219
+ case format
220
+ when "cdl" then emit_cdl($stdout, tree, tree["name"])
221
+ when "json" then puts JSON.pretty_generate(tree)
222
+ when "yaml" then puts YAML.dump(tree)
223
+ end
data/bin/ncvar ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env ruby
2
+ # ncvar — dump a single variable's data from a NetCDF file.
3
+ #
4
+ # Default output is a raw flat byte stream on $stdout, in the variable's
5
+ # native nc_type and the host's native byte order. Pipe it into
6
+ # tools that read binary directly (od, hexdump, numpy.fromfile, ...).
7
+ #
8
+ # $ ncvar FILE.nc temperature # raw binary
9
+ # $ ncvar -f text FILE.nc temperature # one value per line
10
+ # $ ncvar -f json FILE.nc temperature # {shape, type, data}
11
+ #
12
+ # Hyperslab (zero-based, like libnetcdf):
13
+ #
14
+ # $ ncvar -s 0,0 -c 10,20 FILE.nc temperature # 10x20 corner
15
+ # $ ncvar -s 0,0 -c 10,20 -S 1,2 FILE.nc temperature
16
+ #
17
+ # Groups: variable name may be a slash-path, e.g. "/observation/temp".
18
+
19
+ require "carray-netcdf"
20
+ require "json"
21
+ require "optparse"
22
+
23
+ TYPE_NAMES = {
24
+ NetCDF::NC_BYTE => "byte",
25
+ NetCDF::NC_UBYTE => "ubyte",
26
+ NetCDF::NC_CHAR => "char",
27
+ NetCDF::NC_SHORT => "short",
28
+ NetCDF::NC_USHORT => "ushort",
29
+ NetCDF::NC_INT => "int",
30
+ NetCDF::NC_UINT => "uint",
31
+ NetCDF::NC_INT64 => "int64",
32
+ NetCDF::NC_UINT64 => "uint64",
33
+ NetCDF::NC_FLOAT => "float",
34
+ NetCDF::NC_DOUBLE => "double",
35
+ NetCDF::NC_STRING => "string",
36
+ }
37
+
38
+ def parse_int_list(s)
39
+ return nil if s.nil?
40
+ s.split(",").map { |x| Integer(x) }
41
+ end
42
+
43
+ # Resolve "/grp/sub/varname" (or bare "varname") to [grpid, varid].
44
+ def resolve_var(ncid, spec)
45
+ if spec.include?("/")
46
+ grp_path, _, name = spec.rpartition("/")
47
+ grp_path = "/" if grp_path.empty?
48
+ gid = NetCDF.nc_inq_grp_full_ncid(ncid, grp_path)
49
+ raise "group not found: #{grp_path}" if gid.nil?
50
+ else
51
+ gid = ncid
52
+ name = spec
53
+ end
54
+ vid = NetCDF.nc_inq_varid(gid, name)
55
+ raise "variable not found: #{spec}" if vid.nil?
56
+ [gid, vid]
57
+ end
58
+
59
+ format = "bin"
60
+ start_arg = count_arg = stride_arg = nil
61
+ OptionParser.new { |o|
62
+ o.banner = "usage: ncvar [options] FILE.nc VAR"
63
+ o.on("-f FMT", "--format FMT", %w[bin text json],
64
+ "output format: bin (default), text, json") { |v| format = v }
65
+ o.on("-s LIST", "--start LIST", "comma-separated start indices") { |v| start_arg = v }
66
+ o.on("-c LIST", "--count LIST", "comma-separated counts") { |v| count_arg = v }
67
+ o.on("-S LIST", "--stride LIST", "comma-separated strides") { |v| stride_arg = v }
68
+ }.parse!
69
+
70
+ path, varspec = ARGV
71
+ abort "usage: ncvar [options] FILE.nc VAR" unless path && varspec
72
+
73
+ ncid = NetCDF.nc_open(path, NetCDF::NC_NOWRITE)
74
+ begin
75
+ gid, vid = resolve_var(ncid, varspec)
76
+ _, xtype, _, dimids, _ = NetCDF.nc_inq_var(gid, vid)
77
+ shape_full = dimids.map { |d| NetCDF.nc_inq_dimlen(gid, d) }
78
+
79
+ start = parse_int_list(start_arg) || [0] * shape_full.size
80
+ count = parse_int_list(count_arg) || shape_full.zip(start).map { |full, s| full - s }
81
+ stride = parse_int_list(stride_arg)
82
+
83
+ abort "start/count/stride rank mismatch (var has #{shape_full.size} dims)" \
84
+ unless start.size == shape_full.size &&
85
+ count.size == shape_full.size &&
86
+ (stride.nil? || stride.size == shape_full.size)
87
+
88
+ shape = count.dup
89
+
90
+ # An NC_STRING read returns a nested Array<String> and takes no buffer.
91
+ # Anything else comes back as a CArray: layer 1 types it from the
92
+ # variable (an enum by its base type, NC_CHAR as bytes). Opaque has no
93
+ # CArray type, so it is read into bytes, opaque_size to an element.
94
+ nc = xtype == NetCDF::NC_STRING ? NetCDF : CA::NetCDF
95
+ opaque = xtype >= NetCDF::NC_FIRSTUSERTYPEID &&
96
+ NetCDF.nc_inq_user_type(gid, xtype)[4] == NetCDF::NC_OPAQUE
97
+ buf = opaque ? [CArray.uint8(*(count.empty? ? [1] : count), NetCDF.nc_inq_type(gid, xtype)[1])] : []
98
+ data =
99
+ if stride
100
+ nc.nc_get_vars(gid, vid, start, count, stride, *buf)
101
+ elsif start.all?(&:zero?) && count == shape_full
102
+ nc.nc_get_var(gid, vid, *buf)
103
+ else
104
+ nc.nc_get_vara(gid, vid, start, count, *buf)
105
+ end
106
+
107
+ if xtype == NetCDF::NC_STRING
108
+ # data is an Array (possibly nested per ndim) of Ruby Strings.
109
+ flat = data.is_a?(Array) ? data.flatten : [data]
110
+ case format
111
+ when "bin"
112
+ $stdout.binmode
113
+ $stdout.write(flat.join("\0"))
114
+ when "text"
115
+ flat.each { |s| puts s }
116
+ when "json"
117
+ puts JSON.pretty_generate({
118
+ "name" => varspec,
119
+ "type" => "string",
120
+ "shape" => shape,
121
+ "start" => start,
122
+ "data" => data,
123
+ })
124
+ end
125
+ else
126
+ # data is a CArray; its bytes are the native-endian stream.
127
+ bytes = data.dump_binary
128
+ case format
129
+ when "bin"
130
+ $stdout.binmode
131
+ $stdout.write(bytes)
132
+ when "text"
133
+ if xtype == NetCDF::NC_CHAR
134
+ $stdout.write(bytes)
135
+ puts unless bytes.end_with?("\n")
136
+ else
137
+ abort "type #{xtype} not supported for text output" if opaque
138
+ data.to_a.flatten.each { |v| puts v }
139
+ end
140
+ when "json"
141
+ payload =
142
+ if xtype == NetCDF::NC_CHAR
143
+ bytes
144
+ else
145
+ abort "type #{xtype} not supported for json output" if opaque
146
+ data.to_a.flatten
147
+ end
148
+ out = {
149
+ "name" => varspec,
150
+ "type" => TYPE_NAMES[xtype] || NetCDF.nc_inq_type(gid, xtype).first,
151
+ "shape" => shape,
152
+ "start" => start,
153
+ "data" => payload,
154
+ }
155
+ puts JSON.pretty_generate(out)
156
+ end
157
+ end
158
+ ensure
159
+ NetCDF.nc_close(ncid)
160
+ end