carray-netcdf 1.0.14 → 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 -26
- data/API.md +0 -270
- data/Rakefile +0 -12
- data/bin/ncdef2yaml +0 -60
- data/carray-netcdf.gemspec~ +0 -23
- 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/carray-netcdf/netcdf.rb +0 -848
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71bb04b44cd8c06925d970fb1e69ebdea46b7320aeec1e3c542aca3668a66cfc
|
|
4
|
+
data.tar.gz: 864e70e40ec87e90200e74ca97590e7449808dd8e5d012ce64aac8805182733f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|