Repository navigation
Specify how non-finite numbers are encoded in attributes - #398
bendichter wants to merge 2 commits into
Conversation
RFC 8259 has no representation for NaN or the infinities, and the specification gives a rule for them only in fill_value. Implementations have filled the gap in different ways: zarr-python writes the bare tokens NaN and Infinity, which strict JSON parsers refuse, others write the string "NaN", and others write null. The new Attribute values section says that a non-finite number in attributes is written as "NaN", "Infinity" or "-Infinity", the strings already defined for fill values. A string with the same characters is written with the prefix _str_, and a reader removes one prefix, so every value can be read back as it was written. Readers are asked to keep accepting the bare tokens so that existing data stays readable. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
this comes from python's JSON library, and zarr-python's participation in it is something I want to deprecate. see zarr-developers/zarr-python#4400. unfortunately, downstream packages (xarray) rely on this deviant behavior, which is why we still support it.
I don't think the spec should say anything about the contents of |
|
Thanks for the quick look. I agree a convention can carry the encoding, and I am glad to move that part to zarr-conventions, reusing the fill value encodings as you suggest. I do think one piece belongs here. The divergence in the survey does not come from users choosing different encodings. It comes from what each implementation does when it is handed a float NaN: zarrita.js and zarr-java write the string One question on the convention route: would zarr-python be open to applying a registered convention on write, for example as another value of |
I think this sentence in the spec covers that: zarr-specs/docs/v3/core/index.rst Lines 450 to 451 in ad8fc8d
probably not. i think zarr-python should treat |
|
That is clear, thank you. I have written the encoding up as a convention: https://github.com/catalystneuro/zarr-non-finite-attributes. It reuses the fill value strings as you suggested, adds an escape so that a string with the same characters can still be stored, and applies only to a node that registers it, so the meaning of existing data does not change. The repository has reference implementations in Python and JavaScript, test vectors, and the survey from this PR. It is a proposal and I have not tagged a version, so comments on the wording are welcome there. One thing I was unsure about is whether it fits the conventions framework, since it defines how the values of a node's attributes are encoded and has no properties of its own. I will leave this PR open for a few days in case others see a reason to address this in the core specification, and close it otherwise. |
This adds a short section to the v3 core specification that says how
NaN,Infinityand-Infinityare written inattributes. It addresses #141, whose last comment said a PR adding a representation would be welcome.The Problem
The specification defines metadata documents with the JSON type system of RFC 8259, which has no representation for a number that is not finite. It gives
fill_valuea rule for this (the strings"NaN","Infinity"and"-Infinity"), but says nothing forattributes. Implementations have each filled the gap differently, and the results do not interoperate.zarr-python writes the bare tokens
NaNandInfinity, which are not valid JSON. Most other implementations parsezarr.jsonwith a strict JSON parser and cannot open a group or array that has one such attribute. We ran into this from two directions: zarrita.js could not open NWB files that hold"resolution": NaN, and Icechunk refuses to store the same metadata.The table below summarizes a survey of what implementations do today. The details, with versions and sources, are at the end.
"NaN"NaNnull, silentlyNaNattributeTwo changes outside this repository make the question timely. zarr-developers/zarr-python#4400 adds a setting that will eventually make zarr-python refuse non-finite attributes, and it does not yet say what a writer should produce instead. Faster JSON libraries for Python (orjson and msgspec) reject the bare tokens on reading and write
nullin their place, so a library that changes its JSON backend stops reading the files that zarr-python writes today.The Proposal
A non-finite number in
attributesis written as one of the three strings that the specification already defines for fill values:"NaN","Infinity"or"-Infinity". This applies to every value withinattributes, at any depth.A fill value has the array's data type to say that the string denotes a number. An attribute has nothing, so a rule is needed for a string that has the same characters. Such a string is written with the prefix
_str_, and a reader removes one prefix:zarr.json"NaN""-Infinity"NaN"_str_NaN"_str_NaN"_str__str_NaN"_str__str_NaN"_str__str__str_NaN"_str_hello"_str_hello"nan,infandN/AOnly a string that is zero or more
_str_prefixes followed by exactly one of the three reserved strings is affected, so no other string changes. An affected string always gains exactly one prefix when it is written and loses one when it is read, however many it already has. The encoding is reversible for every value. A reference implementation of about twenty lines is here:Reference implementation
The section also says that a reader should accept the bare tokens within
attributes, so that data zarr-python has already written stays readable.Why Strings and Not a Tagged Object
The discussion in #141 leaned toward an object such as
{"@type": ...}, and the objection to plain strings there and in zarr-developers/zarr-python#412 was that a number could not be told apart from a string. I think strings with an escape rule are the better choice now, for these reasons.{"a": NaN, "d": "NaN"}as{"a": "NaN", "d": "NaN"}.float("NaN")in Python andNumber("NaN")in JavaScript convert the string, where an object would need handling written for it.nullis not a workable alternative. It loses the value, and of the readers we tested, hdmf rejects it, netCDF-C returns the textnull, and GDAL returns an empty string.Compatibility
"NaN"stands for a number, starts to be read as a number by every implementation that follows this section."NaN"in an attribute to a number, apart from netCDF-C with its own type annotations. Every reader therefore needs a small change whichever encoding is chosen.Open Questions
_str_describes itself to someone who meets it unprepared. A single_is shorter and matches the leading underscore the ecosystem already treats as reserved. I do not have a strong preference.attributesonly. A non-finite number could in principle appear in the configuration of a codec or another extension, and one rule for the whole document outsidefill_valuemay be simpler.Survey Details
Rows marked "tested" were run with the version given. Rows marked "source" come from reading the implementation's source and were not run.
Python and C libraries
NaN"NaN"asNaNNaN, with no special handling of attributesNaN; the fill value is"NaN""NaN","Infinity","-Infinity"zarr3drivernullNaNJSON libraries for Python, all tested:
json(CPython 3.12) andujson6.0.0 write and read the bare tokens.orjson3.13.0 andmsgspec0.22.0 writenulland reject the bare tokens.simplejson4.2.0 raises on writing and rejects the bare tokens.Other implementations
NaNJSON.parse, andJSON.stringifywith a replacer"NaN"JSON.parseandJSON.stringifynullnull"NaN""NaN"response.json()nullZarr.jl depends on the major version of JSON.jl that is installed, and I have left it out of the summary for that reason. From its source, it writes
nullwith the older version and raises with the newer one, and it rewrites some bareNaNtokens as strings before parsing.Related
np.float32(np.nan)is not a valid attribute value zarr-python#3478 and feat(attributes): warn when writing attributes that are not valid JSON zarr-python#4400, the current work in zarr-pythonnp.nanandmath.nanin attributes earth-mover/icechunk#1238