Repository navigation
fix: preserve non-canonical NaN payloads and distinguish -0.0 in v3 metadata - #4486
barlowa124 wants to merge 9 commits into
Conversation
…etadata float_from_json_v3 decoded hex strings through Python float64, which cannot carry a float16 payload and risks narrowing float32 payloads; decode into a same-width NumPy scalar via np.frombuffer instead. float_to_json_v3 flattened every NaN to "NaN" on write; emit the hex bit pattern for NaNs that are not the canonical positive NaN so the document round-trips the payload it parsed. ArrayV3Metadata.__eq__ compared to_dict() dictionaries, under which -0.0 == 0.0 and (before this change) any NaN payload == "NaN" even though the documents serialize differently. Compare the canonical serialized JSON text instead, which is also the exact form __hash__ already hashes, making the two consistent by construction. Partial fix for zarr-developers#4453 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The strict bits==canonical check serialized float32 0xffc00000 as hex, but the canonical NaN designation covers the payload, not the sign; mask the sign bit so 0xffc00000 writes as "NaN" like the canonical form. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
thanks for this, I will have a look in the next few days! |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4486 +/- ##
==========================================
+ Coverage 94.69% 94.71% +0.01%
==========================================
Files 94 94
Lines 13619 13666 +47
==========================================
+ Hits 12897 12944 +47
Misses 722 722
🚀 New features to boost your workflow:
|
d-v-b
left a comment
There was a problem hiding this comment.
- Compare all of a NaN's bits, sign included, before writing
"NaN". Any other NaN, such as0xffc00000, should be written as hex, as zarrs and tensorstore do. At the moment0xffc00000writes"NaN"and reads back as0x7fc00000. - Make
ArrayV3Metadata.__eq__work withoutjson.dumps(..., sort_keys=True). It raisesTypeErrorwhen attribute keys mixintandstr, and that reachesArray.__eq__too. - Get the canonical NaN bits from numpy (
np.asarray(np.nan, dtype=...)viewed as the matching uint) instead of hardcoded tables. - Use parametrize in
test_noncanonical_nan_serializes_as_hex, and add tests for the0xffc00000round trip and the complex path. - note in the changelog that equality now compares the JSON text, so
1and1.0in attributes no longer compare equal.
|
Pushed 8a2714a.
|
|
d238278 fixes the CI fallout: |
Summary
Third item of #4453: float fill values holding non-canonical NaN payloads do not round-trip, and
ArrayV3Metadata.__eq__conflates documents whosefill_values serialize differently.Two bugs, one in each direction.
Parse to serialize loses NaN payloads.
"0x7fc00001"parses correctly, butfloat_to_json_v3flattened every NaN to"NaN"on write, so the document came back with the canonical payload.float_to_json_v3now emits the hex bit pattern for any NaN that is not the canonical positive NaN (payload or sign differing), which is the case the spec's hex encoding exists to name: "the only way to specify a NaN value other than the specific NaN value denoted by 'NaN'". The old comment claiming NumPy cannot represent distinct NaNs was wrong.np.float32holds payload bits. They were being discarded on write, not absent.A related latent loss is fixed in the same place.
float_from_json_v3decoded hex viastruct.unpackinto Pythonfloat(64-bit), which cannot carry a float16 payload and narrows float32 payloads. Hex strings now decode throughnp.frombufferinto a same-width NumPy scalar, so all three widths round-trip.Equality conflated distinct documents.
ArrayV3Metadata.__eq__comparedto_dict()dictionaries, under which-0.0equals0.0and every NaN payload equals"NaN"(pre-fix).__eq__now comparesjson.dumps(to_dict(), sort_keys=True), the exact string__hash__already hashed, so equal implies equal-hash by construction rather than by parallel convention.For reviewers
float_from_json_v3now returnsnp.floatingfor hex input instead offloat. Both call sites,float.pyandcomplex_float_from_json_v3, consume it through a cast orcomplex(), so there is no caller-visible change."NaN"still denotes and round-trips the canonical positive NaN at each width. v2 serialization is untouched. Complex fill values go through the same helpers per component, so a complex64 real part holding 0x7fc00001 now writes and re-parses as "0x7fc00001" as well. That path was exercised but has no dedicated test. A float16 document declaring a wider hex fill still parses and casts as before. Payload bits survive only when the hex width matches the dtype width, which is what the spec requires anyway.Author attestation
TODO
docs/user-guide/*.md(no user-facing behavior beyond spec compliance)changes/