netcdf_reader
Stable type · Native arrays and readers
Tier: stable · Kind: type · Topic: Native arrays and readers
| Python | Julia | Rust |
|---|---|---|
NetCDFReader | NetCDFReader | NetcdfReader |
Python
class NetCDFReader
NetCDFReader.formats() -> List[str]
NetCDFReader.extensions() -> List[str]
NetCDFReader.open(blob_path: Any) -> Any
NetCDFReader.dim_length(path: Any, dim: str) -> Optional[int]
NetCDFReader.read_native(handle: Any, variables: Optional[Sequence[str]] = None, select: Optional[Any] = None, records: Optional[Any] = None, **_: Any) -> NativeDataset
The active netcdf reader, backed by xarray (netCDF4 engine).
CF-decodes per spec/conformance.md §3, identically to the oracle
(conformance/verify.py): opens with decode_times=False and
mask_and_scale=True, so scale_factor/add_offset are applied in
float64, _FillValue/missing_value cells become NaN, and the time
axis is returned raw (its stored integers) with units+calendar
carried in attrs for ESS. Data variables land in variables;
dimension coordinates (latitude/longitude/time) land in coords.
variables is a projection: an unrequested variable is never read.
select is a DECODE-TIME orthogonal selection — the reader still gets the
whole blob (one URL, one cache key, nothing about the fetch changes) but
materialises only the requested hyperslab. Hence supports_selection = True
while store_backed stays absent/False, which is how a caller tells a
decode-scoped selection from the zarr reader’s fetch-scoped one. See
read_native for the axis rules.
records is RECORD PUSHDOWN, the other half of the refused-time-axis rule:
a select refuses a time axis because record selection belongs to whoever
owns the cadence, and records is how that owner says which records it
wants. See read_native for its semantics and dim_length for
the metadata read that makes its indices computable out of process.
Members
open— Return the blob path as the handle; the dataset is opened inread_nativeunder awithblock so nothing leaks.dim_length— The length of dimensiondimin the blob atpath, orNone.read_native— Decodehandleinto aNativeDataset.
Julia
struct NetCDFReader <: Reader
NetCDFReader()
NetCDFReader()
The netcdf format reader, backed by NCDatasets. CF-decodes per
spec/conformance.md §3: applies scale_factor/add_offset (math in float64),
maps _FillValue/missing_value cells to NaN, and returns the time axis
raw (the stored integers/floats) with units+calendar carried in
attrs — calendar→wall-clock decoding is ESS’s job, never the reader’s.
NCDatasets exposes arrays in column-major (reversed) dimension order; this
reader permutes each array back to file order so field.dims and
size(field.data) match the on-disk layout (and the Python/xarray track).
TEXT variables (char arrays, NetCDF-4 NC_STRING) are string fields like any
other field — projectable, selectable, never skipped (spec/conformance.md §3).
NCDatasets returns a char array as a raw Char array of the full on-disk
shape, which is not what the bytes mean: the classic-NetCDF convention is that
the last dimension is the STRING LENGTH exactly when nothing else claims it as an
axis (it has no coordinate variable of its own and every variable using it is a
char using it last). So char label(n, strlen) decodes to n strings on
dims == ["n"], a 1-D char label(strlen) on a private dimension to a SCALAR
string (dims == []), while char label(n) beside a float value(n) stays n
one-character strings. Trailing NUL padding is stripped and only trailing NULs —
a trailing SPACE is data — and an all-NUL cell is the EMPTY string, never " ".
This is exactly xarray’s CharacterArrayCoder/conventions.stackable rule, so
the three tracks return the same strings for the same bytes.
reader_kwargs: variables=[...] restricts the returned data variables and
is a real PROJECTION PUSHDOWN — an unrequested variable is never decoded, rather
than decoded and thrown away. Coordinate fields are always returned (they are the
native grid the arrays live on). An empty list, like nothing, means every
data variable (spec/conformance.md §3), never none; a requested name absent from
the blob is an error listing what is present.
select=Dict("axes"=>[...]) is a DECODE-TIME orthogonal selection: the reader
still fetches the whole blob (one URL, one cache key — nothing about the fetch or
the cache changes), but materialises only the requested hyperslab, reading only
the intersecting HDF5 chunks off local disk. supports_selection is true while
store_backed stays false, and that pair is how a caller tells a DECODE-scoped
selection from the zarr reader’s FETCH-scoped one.
The vocabulary is the zarr reader’s, unchanged and 0-based: each axis is
"all", Dict("indices"=>[...]) (an explicit, possibly non-contiguous, ordered
index list, returned in the order given) or Dict("slice"=>[start, stop, step?])
(half-open, step defaults to 1). Zero-based is chosen over the AST side’s
1-based convention on purpose: it is ONE select spelling across every reader and
both sibling tracks, so a caller that translates for zarr does not translate
differently here. Applied thus:
- the axes are positional over FILE-order dims (
[time, lev, lat, lon]for a GEOS-FP A3dyn variable, i.e. the orderfield.dimsreports) of every array whose rank equals the axis count — the zarr rule. Rank is the DECODED field’s, so achar label(n, strlen)counts as rank 1: a consumed string length is not an axis, no selector may bind to it, and a selection naming one is an error rather than a silent truncation of every string; - that induces a dimension-name → selector map, which is then applied by
NAME to every other array and to the coordinate fields, so a windowed
variable can never come back beside a full-length
lon/lat. Two arrays of matching rank that disagree about a dimension are an error, not a coin toss; - a
selectwhose time axis is anything but"all"is REFUSED: record selection belongs to the Provider, which owns the cadence (records_per_sample), not to the reader (spec/conformance.md §3); - an axis count matching no array in the blob is an error rather than a silently-ignored selection;
- every resolved index is bounds-checked (
0 <= i < dim_len) for asliceexactly as for anindiceslist: an over-long or negative[start, stop)is an error, never a silent clamp and never a negative wrap-around; - a dimension is never dropped — a one-index axis comes back at length 1 —
and an axis may legally select NOTHING (
Dict("indices"=>[]), or an empty half-openDict("slice"=>[1, 1])), giving a zero-length axis kept indimsrather than an error.
records=Dict("dim"=>"time", "indices"=>[3,4]) is RECORD PUSHDOWN, the other
half of the refused-time-axis rule above. select refuses a time axis because
record selection belongs to whoever owns the cadence; records is how that owner — the
[Provider], which has already turned a cadence tick into this file’s own record
index — says which records it wants, in the file’s own 0-based numbering. The
reader is told the records, never the cadence: it does no mod1, knows nothing
of times or records_per_sample, and simply materialises the named records of
dim (and of dim’s own coordinate) in the order given. Duplicates are legal —
the end-of-data bracket [last, last] is one — and an index outside 0:len-1,
or a dim the blob does not have, is an error rather than a wrapped read.
records and a select may be combined; they must not both name dim.
indices may also be a CALLABLE len -> indices, evaluated against dim’s own
length inside this reader’s open. That is the in-process spelling only (the wire
form is the list), and it exists for one measured reason: the record a cadence
owner wants is a function of the file’s length along dim, so with the list form
alone the caller has to open the blob once to learn that length and the reader
opens it again to read — and on a GEOS-FP A1 file that second open (16.9 ms)
costs MORE than the records it saves (21.4 ms whole → 18.5 ms selected). With the
callable there is exactly ONE open, and the mod1 still runs in the caller’s own
closure. A blob with no dim at all does not call the resolver and makes no
selection, which is what a caller that had probed would have found.