NetCDF Utilities#
Low-level GDAL helpers for attribute reading, no-data extraction, scale/offset handling, time conversion, and dtype utilities.
pyramids.netcdf.utils
#
cf_epoch_units(resolution)
#
The CF units string for offsets counted from the pyramids epoch.
The writers disagree, deliberately, on the resolution they count in: the
collection axis uses nanoseconds because an int64 count at that scale
round-trips datetime64[ns] exactly, while the xarray interop path uses
fractional seconds because it must also carry NaT as NaN. What they
must not disagree on is the epoch, which is why only that half is shared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolution
|
str
|
A CF time unit name, e.g. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
|
Note
Both readers now parse every resolution this builds, and they preserve
different amounts of it. :func:pyramids.netcdf.utils.decode_cf_time
scales the offsets to datetime64[ns] in integer nanoseconds, so the
"nanoseconds since ..." axis DatasetCollection.to_netcdf writes
comes back exact -- which is the point of writing at that resolution.
:func:pyramids.netcdf.utils.create_time_conversion_func formats
through datetime, whose finest unit is the microsecond, so a
nanosecond axis is rounded there; the residual is visible only in a
%f format.
This is worth stating because it was wrong twice. The docstring first
claimed decode_cf_time parsed all of them (it raised on
nanoseconds, because it handed the string to cftime); the
correction then recorded that gap as if it were the design. It was a
defect: is_cf_time_units accepted the axis, so the writer produced a
file the reader refused. The test that should have caught either
version asserted cf_epoch_units(r) == f"{r} since {CF_EPOCH}" against
a function whose body is that f-string -- so it could not fail,
whatever the string was for.
Examples:
- The collection axis counts in nanoseconds:
- The interop path counts in seconds, from the same epoch -- which is the whole reason the epoch is shared and the resolution is not:
- Whatever the resolution, the epoch behind it is the same one, which is what lets two axes written at different scales be compared:
See Also
pyramids.netcdf.utils.create_time_conversion_func: Formats any of these
strings back to text, at microsecond resolution.
pyramids.netcdf.utils.decode_cf_time: Decodes them to datetime64[ns],
at the full nanosecond resolution -- see the note above.
Source code in src/pyramids/base/_cf_epoch.py
resolve_full_name(obj, group_full_name, fallback_name)
#
Full hierarchical name of a GDAL dimension / array, with a qualified fallback.
Tries obj.GetFullName(); when that raises (some drivers don't expose it on every
object), composes the path from the parent group's full name and fallback_name —
"<group>/<name>", or "/<name>" when the parent is the root group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
Any
|
A GDAL dimension or MDArray. |
required |
group_full_name
|
str
|
Full name of the parent group (e.g. |
required |
fallback_name
|
str
|
The object's short name, used to build the fallback path. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The resolved full-name string. |
Source code in src/pyramids/netcdf/utils.py
create_time_conversion_func(units, out_format='%Y-%m-%d %H:%M:%S', calendar='standard')
#
Create a converter that maps numeric CF time offsets to date strings.
Parses CF-compliant time unit strings (e.g.,
"days since 1979-01-01") and returns a callable that
converts numeric offsets to formatted date strings.
The fast datetime + timedelta path is taken only for the Gregorian
family (standard, gregorian, proleptic_gregorian) on an origin at or
after the 1582-10-15 reform. Everything else is resolved by
cftime.num2date(): every other calendar (julian, noleap, 365_day,
all_leap, 366_day, 360_day), and — whatever the calendar is called,
proleptic_gregorian included — any origin that predates the reform, since
timedelta counts proleptic Gregorian days from it while a standard axis
counts Julian ones. On "hours since 1-1-1 00:00:0.0" the offset 17549208
is 2003-01-01 under standard and 2003-01-03 under
proleptic_gregorian; the two agree only from 1582-10-15 on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
str
|
CF time unit string in the format
|
required |
out_format
|
str
|
strftime format for the output strings.
Defaults to |
'%Y-%m-%d %H:%M:%S'
|
calendar
|
str
|
CF calendar type. Defaults to |
'standard'
|
Returns:
| Name | Type | Description |
|---|---|---|
Callable |
Callable
|
A function that takes a numeric value and returns a formatted date string. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the unit string cannot be parsed or uses an unsupported time unit. |
Examples:
-
Convert day offsets from a 1979 origin:
-
Use hour-based units with a custom format:
-
A pre-1582 origin is resolved by calendar, not by
timedelta, so the mixedstandardcalendar and the proleptic one answer two days apart: -
A 360-day calendar counts twelve 30-day months, so day 360 is a year on:
See Also
_parse_units_origin: Parses the unit string.
_origin_precedes_reform: Decides whether the origin needs cftime.
Source code in src/pyramids/netcdf/utils.py
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 | |
cf_units_text(units)
#
The units attribute as text, or None when it is not text at all.
A units read straight out of an HDF5 / netCDF attribute can arrive as
bytes. That is the same unit, undecoded -- not a different kind of thing
-- so both the predicate below and the decoder that gates on it resolve it
here rather than each deciding for itself. Answering "not a time unit" for
bytes put them back in the hole the predicate exists to fill; decoding them
in the predicate alone would have let decode_cf_time hand the raw bytes
to cftime, which raises.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
Any
|
The CF |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
str | None: The text, or |
Examples:
- A string is itself, and bytes are decoded:
- Anything that is not text at all answers
None, rather than raising the way a bare" since " in unitwould:
See Also
is_cf_time_units: Asks whether that text names a CF time axis.
Source code in src/pyramids/netcdf/utils.py
is_cf_time_units(units)
#
True when units declares a CF time axis.
Four places asked this and answered differently. Axis detection matched a
bare lowercased "since" substring; decoding required a case-sensitive
" since ". A file whose units read "Days SINCE 1970-01-01" -- which
cftime parses perfectly well -- was therefore reported as a time axis and
then handed back as raw numbers, with nothing raised. This is the one rule
both questions now use.
Total: anything that is not a string is not a time unit. GDAL attributes
are normalised into scalars or lists, so a units written as a
one-element array is a real input, and the CF compliance checker exists to
report on malformed files rather than to crash on one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
str | bytes | None
|
The CF |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True when |
Examples:
- The ordinary form, and the uppercase one that used to decode wrong:
- A spatial unit is not a time axis, nor is a missing one:
- Neither is a
unitsa malformed file wrote as a list or a number: sincehas to be a word with a period and an epoch around it, so a unit that merely contains the letters is not matched:- Bytes are decoded first, so an attribute that arrived undecoded still names the axis it names:
See Also
decode_cf_time: Decodes the axes this identifies.
Source code in src/pyramids/netcdf/utils.py
decode_cf_time(values, unit, calendar='standard', context=None)
#
Decode numeric CF time offsets to datetimes.
Standard / gregorian / proleptic_gregorian calendars yield datetime64[ns] when
representable; non-standard calendars (360_day / noleap …) yield cftime
objects. Arrays whose unit is not a "<interval> since <origin>" string are
returned unchanged.
A Gregorian-family axis is decoded by adding its offsets to the origin in integer
nanoseconds, rather than through cftime. Behaviour differs from earlier releases
in two ways, both of them consequences of dropping cftime's microsecond floor:
a "nanoseconds since …" axis decodes instead of raising, and a NaN offset
decodes to NaT instead of to the origin. Both decode paths agree about missing
values: the cftime fallback honours the mask cftime returns, so a NaN or an
inf offset is missing there too rather than silently reading as the origin (#1116).
Anything the integer path cannot take exactly -- an unparseable origin, a period such as
"months", an instant the integer nanosecond scale cannot reach, or a pre-1582 origin
on a mixed Julian/Gregorian calendar -- still goes to cftime. That reach is not a
fixed span of years: the gate sums the offset's magnitude and the origin's distance from
1970, so a far-from-1970 epoch shortens it (off days since 1900-01-01 it gives out in
March 2115). That is a wider range than the integer path's, not a narrower one: a date
cftime decodes is still cast to datetime64[ns] whenever it fits, so 2255-2262 --
past the integer scale but inside the type -- comes back as datetime64 all the same.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
ndarray
|
The numeric values already read for the coordinate. |
required |
unit
|
str | bytes | None
|
The coordinate's CF unit string (e.g. |
required |
calendar
|
str
|
The CF calendar name. Defaults to |
'standard'
|
context
|
str | None
|
Optional name of the axis being decoded, used only so the out-of-range warning can say which axis it is about. A store with several time axes otherwise warns with nothing but the units string to tell them apart. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray
|
np.ndarray: Decoded datetimes for a time axis, else |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Examples:
- A date past what
datetime64[ns]holds keeps its real value, and says so:>>> import warnings >>> import numpy as np >>> from pyramids.netcdf.utils import decode_cf_time >>> with warnings.catch_warnings(record=True) as caught: ... warnings.simplefilter("always") ... decoded = decode_cf_time( ... np.array([400_000]), "days since 1970-01-01", "standard" ... ) >>> decoded.dtype, decoded[0].year (dtype('O'), 3065) >>> caught[0].category.__name__ 'UserWarning' - The resolution the collection writer counts in, read back with its
sub-microsecond digits intact:
>>> import numpy as np >>> from pyramids.netcdf.utils import cf_epoch_units, decode_cf_time >>> offsets = np.array([0, 1577836800123456789], dtype="int64") >>> decoded = decode_cf_time( ... offsets, cf_epoch_units("nanoseconds"), "proleptic_gregorian" ... ) >>> decoded.astype(str).tolist() ['1970-01-01T00:00:00.000000000', '2020-01-01T00:00:00.123456789']
See Also
is_cf_time_units: The predicate this gates on. It accepts every
"<period> since <origin>" string, so a resolution it admits and
this cannot decode is a defect -- which is what a nanoseconds
axis was, accepted by the predicate and refused by cftime.
Source code in src/pyramids/netcdf/utils.py
1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 | |
encode_cf_time(value, unit, calendar='standard')
#
Convert a date string / datetime to a coordinate's numeric CF scale.
The inverse of :func:decode_cf_time for a single value: used to translate a
user-supplied selection bound back to the time axis's stored numbers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
A date string, :class: |
required |
unit
|
str
|
The CF unit string the numeric scale is expressed in. |
required |
calendar
|
str
|
The CF calendar name. |
'standard'
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
|
Source code in src/pyramids/netcdf/utils.py
read_cf_attributes(obj)
#
Read a GDAL object's attributes the way a CF consumer needs them.
The CF reader for every consumer of units / calendar / standard_name /
axis. Use this rather than :func:_read_attributes wherever the attributes are
interpreted as CF: attributes alone omit units, because GDAL moves it to the object's
unit slot, so a site that reads them raw silently loses it — which is how a time axis came
back undecodable, CF axis detection by units never fired, and a UGRID variable's unit
vanished on a round trip (#1078).
:func:_read_attributes remains the right call for a verbatim view of what the file
declares (serialisation, round-trip writers); this one is for interpreting CF.
The slot is the CF units for the netCDF and HDF5 drivers, which is where this matters.
Another multidim driver (Zarr, HDF-EOS) may fill it from a format-specific field, in which
case the reported units is that driver's unit rather than a CF attribute the file
literally declares — a reason to read attributes raw when the question is "what does this
file say", not "what does this axis mean".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
Any
|
Any GDAL object exposing |
required |
Returns:
| Type | Description |
|---|---|
dict[str, AttributeValue]
|
dict[str, AttributeValue]: The object's attributes, including |