Repository navigation
Prefer metadata over store reads in _decode_cf_datetime_dtype - #11304
Conversation
|
@aladinor Sorry for the delay here. I've responded in the linked issue: Any chance you could have a look? That way we could separate concerns and keep fully backwards compatible. Thanks! |
Restructure dtype inference into a metadata-first path that falls back to reading the first and last values only when metadata cannot prove safety. For well-formed data the fast path runs decode_cf_datetime on a synthetic [0, 1] array so resolution-selection logic matches real-data output. Adds _verify_decoded_dtype to CFDatetimeCoder.decode so an overflow mismatch between declared and actual datetime dtype raises a clear TypeError instead of silently corrupting dask map_blocks output. Addresses pydata#11303.
Cover all paths that should skip store reads (explicit use_cftime, non-standard calendar, wide time_unit, non-chunked data, narrow storage dtype bounds, valid_max / time_coverage_end attrs) plus the chunked int64 fallback and malformed units error. Uses InaccessibleArray to prove no reads occur on the fast paths and a TypeError is raised for the verification wrapper on dtype kind mismatches.
- Restrict the "skip reads" fast path to backend-lazy data (LazilyIndexedArray, walked through intermediate coder wrappers). In-memory PandasIndexingAdapter arrays now keep the eager first/last reads so fail-fast semantics for overflow values are preserved (fixes test_decode_cf_datetime_transition_to_invalid regression). - Short-circuit use_cftime=True and non-standard calendars in the metadata path so cftime is not required for the fast path (fixes bare-min CI failure). - Drop the `_verify_decoded_dtype` guard. Instead rely on the fallback path's reads to surface the correct cftime dtype when the metadata-based check cannot prove safety. - Rename the transform-wrapping intermediate to avoid the mypy reassignment error on CFDatetimeCoder.decode.
- Replace the "non-chunked data" test with tests that exercise the new LazilyIndexedArray check, including coder-wrapped lazy backends. - Add a regression test for in-memory PandasIndexingAdapter data to ensure it still falls through to the eager reads path. - Drop the verification-wrapper tests along with the wrapper itself.
The wording still referred to the old "chunked data" criterion. Update it to describe the current backend-laziness check.
Drop the mention of the runtime dtype-mismatch guard (removed) and reference the backing issue.
Following review feedback, swap the auto-detection of when to skip data reads (which silently changed semantics for some edge cases) for an explicit ``infer_dtype`` parameter on ``CFDatetimeCoder``: - ``infer_dtype="data"`` (default): unchanged, original behavior of reading first/last to verify decoding and fall back to cftime when needed. - ``infer_dtype="metadata"`` (new opt-in): infer the dtype from ``units`` / ``calendar`` / ``use_cftime`` / ``time_unit`` alone, no data reads. Short-circuits cases that don't need ``cftime`` installed; the standard-calendar auto path uses a synthetic ``[0, 1]`` decode to match what real-data decoding would produce for in-range values. Units are still validated up front for fail-fast parity with the data path. Drops the ``LazilyIndexedArray`` walk, the ``valid_max``/storage-dtype bound helpers, and the ``_CannotInferFromMetadata`` sentinel — all machinery that propped up the implicit auto-detection. The result is ~150 lines lighter and fully backwards compatible.
Tests now exercise the explicit ``CFDatetimeCoder(infer_dtype="metadata")`` opt-in instead of the removed auto-detection. Each test wraps the variable's data in ``InaccessibleArray`` to prove no reads happen on the metadata path, and a paired ``test_data_default_still_reads_first_and_last`` locks in the backwards-compatible behavior of the default ``"data"`` path.
Replace the ``_wrap_decode_error`` helper with a module-level message template and inline the ``ValueError`` raise at each of the three call sites. Makes the error path obvious at the point it happens — small win in clarity, no behavior change.
2dcf9ce to
a8b39e9
Compare
|
@kmuehlbauer, I've implemented what you suggested. Please let me know your thoughts. |
|
Quick update — the branch is now merged with the latest
The 9 CI test-job failures and the
Whenever you have time for another look, @kmuehlbauer, much appreciated! |
|
Is the assumption here that @requires_cftime
@pytest.mark.parametrize("calendar", [None, "standard", "noleap", "360_day"])
@pytest.mark.parametrize("use_cftime", [None, True, False])
@pytest.mark.parametrize("time_unit", ["s", "ms", "us", "ns"])
def test_metadata_matches_data_path(self, calendar, use_cftime, time_unit):
# The contract of infer_dtype="metadata" is that it predicts what
# infer_dtype="data" produces without reading the data: the same
# dtype on success, or the same failure. The data path is the oracle.
attrs = {"units": "days since 2000-01-01"}
if calendar is not None:
attrs["calendar"] = calendar
variable = Variable(("t",), np.array([0, 1, 2], dtype="int64"), attrs=attrs)
def decode(infer_dtype):
coder = CFDatetimeCoder(
use_cftime=use_cftime, time_unit=time_unit, infer_dtype=infer_dtype
)
return coder.decode(variable).dtype
try:
expected = decode("data")
except ValueError:
with pytest.raises(ValueError):
decode("metadata")
else:
assert decode("metadata") == expectedwhich fails wiht e.g.: or are there known limitations that should be documented here? |
- Infer the dtype by decoding a synthetic [0, 1] with the same code as the data path, instead of shortcuts that ignored the resolution needed by the units/reference date (e.g. "milliseconds since" with time_unit="s") or the units cftime cannot decode. - Read the first and last values, as with infer_dtype="data", when dates near the reference date need cftime (e.g. "hours since 1-1-1" holding dates in 2020) or when decoding them fails. - Check every decoded chunk against the inferred dtype and raise a clear ValueError on mismatch, instead of silently casting under dask. - Keep the default path identical to main, including the error message. - Document the behaviour; move the whats-new entry to New Features. - Test parity with the data path over units, reference dates, calendars and storage dtypes, dask and zarr round trips, and the no-read guarantee.
|
Thanks @aladinor for updating the PR and for following-up on @ianhi's comment. I went through the changes and the code flow looks sound. I think this would be a nice performance addition in any case. I'd really like to get @spencerkclark's eyes on this one, too JFTR: Actually, this is a nice showcase how splitting out coders helps adding features. |
|
I agree, this seems reasonable and perhaps should be the default. Since the failure just comes later, and we almost always decode datetim64 eagerly on open, it seems pretty safe. The one exception is the "time bounds" variable which stays lazy till compute-time, but perhaps that's OK. |
|
Thanks, @kmuehlbauer, for your prompt reply. @ianhi, indeed, there were several scenarios where the metadata path didn't match the data path, including the ones mentioned in your comment. I've refactored it to make sure the metadata path now reuses the data path. It decodes and reads only the first and last elements in the array when metadata cannot infer the dtype. It now also raises an error instead of silently casting mismatched values. I also added and extended your proposed test. |
|
@aladinor Please resolve the conflicts, this can go in! |
Description
Closes #11303.
Restructure
_decode_cf_datetime_dtype(xarray/coding/times.py) to infer the decoded dtype from metadata first and fall back to reading the first and last values only when metadata cannot prove safety.The metadata-first path handles explicit
use_cftime, non-standard calendars, non-"ns"time_unit, non-chunked data,valid_max/time_coverage_endattrs that bound values, and narrow integer storage dtypes (int8/16/32) where overflow is provably impossible. Safe paths rundecode_cf_datetimeon a synthetic[0, 1]array so resolution logic matches real-data output.Also adds a runtime
_verify_decoded_dtypeguard onCFDatetimeCoder.decode: ifdecode_cf_datetimelater produces a different datetime/object dtype kind than was declared (the rare overflow case), raiseTypeErrorinstead of letting dask'smap_blockssilently cast into garbage. This turns a latent silent-corruption edge case into a clean error even on the fallback path.Benchmark —
nexrad-arco/KLOTicechunk DataTree, 107 groups, 106 time variablescProfile on unpatched
main:With this PR:
open_datatree_decode_cf_datetime_dtypeReproducer in #11303.
Behavior
decode_times/CFDatetimeCodersignatures unchanged.unitsstill raisesValueErrorat open time (from the metadata path's unit validation).TypeErrorat access time rather than silent garbage — strictly safer.Checklist
_decode_cf_datetime_dtypetriggers 2 store reads per time variable — expensive on remote stores #11303TestDecodeCFDatetimeDtypeintest_coding_times.py, usingInaccessibleArrayto prove no reads on the fast paths)whats-new.rstAI Disclosure
Tools: Claude Code