From 768af53123c7ee43805d31846a55378fc0098401 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Mon, 14 Sep 2026 07:15:16 -0500 Subject: [PATCH] FIX: Keep the column datetime unit when re-binning gauge accumulations _apply_match raised IndexError: dimension coordinate 'time' conflicts between indexed and indexing objects, taking five tests in test_radclss.py down on every platform since #42 merged. The scheduled run hours before that merge was green. A column time coordinate is built from base_time, which carries second resolution. _accumulate_to_grid subtracted a pandas Timedelta from it to form the leading interpolation edge, and to_timedelta64 carries sub-second precision, so the subtraction promoted the whole edges array to nanoseconds. interp handed that unit to the result, and _apply_match then tried to write a nanosecond-stamped result into a second-resolution column. The timestamps were identical; only the unit differed. Whether that is fatal depends on the xarray version, which is why it never reproduced locally. xarray 2025.6.1, which CI pins, compares coordinates by dtype as well as by value and rejects the write. Newer xarray compares by value and accepts it. The existing tests could not catch this: they build column grids with pd.date_range, which is always nanoseconds, so the promotion is invisible. The new test uses a second-resolution grid like the real pipeline and asserts on dtype directly, so it holds on any xarray version rather than relying on one to raise. Co-Authored-By: Claude Opus 5 (1M context) --- src/radclss/util/column_utils.py | 10 ++++++++- tests/test_column_utils.py | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/radclss/util/column_utils.py b/src/radclss/util/column_utils.py index f6e5f27..6b22bac 100644 --- a/src/radclss/util/column_utils.py +++ b/src/radclss/util/column_utils.py @@ -669,7 +669,15 @@ def _accumulate_to_grid(grd_ds, column_time, default_step): """ target = np.asarray(column_time.values).ravel() step = _column_time_step(column_time, default_step) - edges = np.concatenate([[target[0] - step.to_timedelta64()], target]) + # pd.Timedelta.to_timedelta64 carries sub-second precision, so subtracting + # it promotes the leading edge -- and with it the whole array -- off the + # column's own datetime unit. interp hands that unit to the result, and + # _apply_match then assigns the result into a column whose time coordinate + # is still datetime64[s]; xarray compares the two coordinates by dtype as + # well as by value and rejects the write. Stay on the column's unit. + edges = np.concatenate([[target[0] - step.to_timedelta64()], target]).astype( + target.dtype + ) # A zero anchor ahead of the record lets the first output interval # difference against "nothing accumulated yet" rather than against a NaN. diff --git a/tests/test_column_utils.py b/tests/test_column_utils.py index 114bd0e..56c37c0 100644 --- a/tests/test_column_utils.py +++ b/tests/test_column_utils.py @@ -7,6 +7,7 @@ from radclss.util.column_utils import ( _accumulate_to_grid, + _apply_match, _column_time_step, get_nexrad_column, subset_points, @@ -244,6 +245,41 @@ def test_accumulate_to_grid_keeps_gaps_missing(): assert np.isnan(regridded["accum_nrt"].values).any() +def test_accumulate_to_grid_keeps_the_column_datetime_unit(): + """ + A real column time coordinate is built from base_time and so carries + second resolution, not the nanoseconds pd.date_range hands the tests + above. Subtracting a pandas Timedelta promoted the interpolation edges to + nanoseconds, and interp passed that unit on to the result. _apply_match + could then no longer write the result back into the column: xarray + compares coordinates by dtype as well as by value, so the two disagreed + despite holding identical timestamps. + """ + gauge = _synthetic_gauge() + seconds = gauge.time.values.astype("datetime64[s]")[::5] + column_time = xr.DataArray(seconds, dims="time", coords={"time": seconds}) + + regridded = _accumulate_to_grid(gauge, column_time, "5Min") + + assert regridded["time"].dtype == column_time.dtype + + # The write _apply_match performs in the pipeline, which is where the + # mismatched unit actually surfaced. + matched = regridded.assign_coords(station="M1").expand_dims("station") + column = xr.Dataset( + {"accum_nrt": (("station", "time"), np.zeros((1, seconds.size)))}, + coords={"station": ["M1"], "time": seconds}, + ) + + _apply_match(column, "M1", matched) + + np.testing.assert_allclose( + column["accum_nrt"].values[0], + regridded["accum_nrt"].values, + atol=1e-9, + ) + + def test_column_time_step_falls_back_when_unmeasurable(): """Degenerate grids fall back to the supplied default rather than raising.""" single = xr.DataArray(pd.to_datetime(["2025-06-19T00:00"]), dims="time")