Skip to content

fix(dtype): preserve scaled generic time units and normalize the μs alias - #4342

Merged
d-v-b merged 6 commits into
zarr-developers:mainfrom
d-v-b:claude/friendly-williams-cbd545
Sep 24, 2026
Merged

d-v-b merged 6 commits into
zarr-developers:mainfrom
d-v-b:claude/friendly-williams-cbd545

Conversation

@d-v-b

@d-v-b d-v-b commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Preserves the unit, scale, and byte order of NumPy datetime64 / timedelta64 dtypes through native conversion, Zarr metadata, and CPU chunk IO. The largest gap is the generic unit with a scale factor, e.g. np.dtype("M8[2generic]").

Changes

  • Native conversion. DateTime64 and TimeDelta64 read the unit and scale from the dtype object. They previously read dtype.name, which omits a generic unit's scale.
  • V2 metadata. NumPy's dtype.str also omits the generic scale, so V2 serialization appends an explicit suffix, e.g. <M8[2generic]. V3 already stores unit and scale in its configuration.
  • μs alias. The constructor stores μs as us, NumPy's spelling. Without this an instance built with μs compares unequal to itself after a round trip through NumPy. μs is still accepted as input and in stored metadata.
  • CPU buffer allocation. NumPy allocation drops a generic time dtype's scale and byte order (np.zeros(1, ">M8[2generic]").dtype is <M8). cpu.NDBuffer.create and empty restore the requested dtype with a view before filling.
  • CPU buffer astype. NumPy's astype between generic time dtypes ignores the target and keeps the source byte order. cpu.NDBuffer.astype converts endian-only generic casts through the integer counts.
  • Fill values. Integer fill values for generic datetimes decode through datetime_from_int, since NumPy's scalar constructor rejects integer counts with a generic unit. Structured defaults build a zero count for generic datetime fields.
  • NaT scalars. default_scalar and the NaT branch of TimeDelta64.cast_scalar built NaT in the bare unit, dropping the scale factor. On NumPy < 2.2, casting a timedelta NaT between scales yields a count, so missing chunks of an m8[2us] array read back as 2**62 - 1. NaT is now built in the dtype's unit and scale.
  • Test strategy. The dtype Hypothesis strategy now generates μs and scaled generic units instead of excluding them.

GPU buffers are not changed or validated.

Compatibility of the V2 suffix

NumPy 1.26, 2.0, 2.2, and 2.5 all parse [Ngeneric] with the correct scale (up to 2**31 - 1) and byte order. NumPy never emits this form, so a reader that re-serializes via dtype.str drops the scale.

zarr 2.18.7 cannot read these arrays correctly:

  • It fails to open any generic datetime array whose fill value is the NaT integer -9223372036854775808, which is what zarr writes by default. This already happens without the suffix (plain >M8), so it is not a regression.
  • With fill_value: null it parses the dtype correctly and returns the right counts, but the result array has scale 1, so a scaled generic array is read with the wrong unit.

Tests

  • Both temporal types, every supported unit, scales 1, 2 and 2**31 - 1, and both byte orders, through native, V2 and V3 conversion.
  • Plain and structured array writes and reads in V2 and V3, including resized regions with missing chunks.
  • CPU allocation for both paths, both orders, and None / 0 / "NaT" fill values.

The allocation, astype, and NaT-scale regression tests fail when the corresponding fix is removed. The time and buffer tests also pass on NumPy 2.0 and 2.2. tests/test_array.py, tests/test_codecs, tests/test_buffer.py, tests/test_dtype, and tests/test_properties.py pass (6,254 passed).

🤖 Generated with Claude Code

@read-the-docs-community

read-the-docs-community Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 zarr-metadata | 🛠️ Build #34589350 | 📁 Comparing df506a1 against latest (ba883a5)

  🔍 Preview build  

1 file changed
± api/v3/data_type/index.html

@codecov

codecov Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.37%. Comparing base (34b7c3d) to head (2ec7acb).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4342      +/-   ##
==========================================
+ Coverage   94.36%   94.37%   +0.01%     
==========================================
  Files          93       93              
  Lines       13142    13171      +29     
==========================================
+ Hits        12401    12430      +29     
  Misses        741      741              
Files with missing lines Coverage Δ
src/zarr/core/buffer/cpu.py 98.90% <100.00%> (+0.25%) ⬆️
src/zarr/core/dtype/npy/structured.py 94.60% <100.00%> (+0.24%) ⬆️
src/zarr/core/dtype/npy/time.py 98.91% <100.00%> (+0.04%) ⬆️
src/zarr/testing/strategies.py 96.62% <100.00%> (-0.05%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@d-v-b d-v-b changed the title fix(dtype): reject scaled generic time units and normalize the μs alias fix(dtype): preserve scaled generic time units and normalize the μs alias Sep 13, 2026
@d-v-b d-v-b added this to the 3.4.1 milestone Sep 16, 2026
…lias

`DateTime64` and `TimeDelta64` read unit and scale from the dtype object
rather than `dtype.name`, which drops a generic unit's scale. V2 metadata
writes an explicit `[Ngeneric]` suffix; the constructor stores `μs` as `us`.

NumPy allocation drops a generic time dtype's scale and byte order, and
`astype` between generic time dtypes ignores the target. The CPU buffer
restores the dtype with a view after allocation and converts endian-only
casts through the integer counts. Integer fill values for generic datetimes
decode through `datetime_from_int`, and structured defaults build a zero
count for generic datetime fields.

Assisted-by: ClaudeCode:claude-opus-5-5
@d-v-b
d-v-b force-pushed the claude/friendly-williams-cbd545 branch from df506a1 to bc12e17 Compare September 24, 2026 15:25
@d-v-b
d-v-b marked this pull request as ready for review September 24, 2026 15:28
Generate the valid native dtypes and V2/V3 JSON for `DateTime64` and
`TimeDelta64` from every unit, scales 1, 2 and 2**31 - 1, and both byte
orders, instead of a separate round-trip test that repeated the harness's
native and JSON round trips.

Assisted-by: ClaudeCode:claude-opus-5-5
`DateTime64.default_scalar`, `TimeDelta64.default_scalar`, and the NaT
branch of `TimeDelta64.cast_scalar` built NaT in the bare unit, dropping
the scale factor. On NumPy < 2.2, casting a timedelta NaT between scales
yields a count instead of NaT, so missing chunks of a `m8[2us]` array
read back as 2**62 - 1 instead of NaT.

Assisted-by: ClaudeCode:claude-opus-5-5
Assisted-by: ClaudeCode:claude-opus-5-5
@d-v-b
d-v-b merged commit d7686f6 into zarr-developers:main Sep 24, 2026
30 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant