Skip to content

Add float16 data type (v3 and v2) - #93

Open
konstibob wants to merge 1 commit into
zarr-developers:mainfrom
konstibob:feat/float16-dtype
Open

Add float16 data type (v3 and v2)#93
konstibob wants to merge 1 commit into
zarr-developers:mainfrom
konstibob:feat/float16-dtype

Conversation

@konstibob

Copy link
Copy Markdown
Contributor

float16 is part of the Zarr v3 core spec, not an optional extension -- the zarr-extensions entry for it is only a pointer back to the core spec's data types section. Before this change, opening any float16 array failed at metadata parse with "Cannot deserialize value of type DataType from String float16".

No new codec is needed: the existing bytes codec handles any fixed-itemsize data type. The work is the in-memory representation, because neither Java nor ucar.ma2 has a 16-bit float. float16 is therefore held as ucar.ma2.FLOAT and converted at the codec boundary, which makes it the first data type whose encoded width (2 bytes) differs from its in-memory width (4 bytes).

float16 is part of the Zarr v3 core spec, not an optional extension -- the
zarr-extensions entry for it is only a pointer back to the core spec's data
types section. Before this change, opening any float16 array failed at metadata
parse with "Cannot deserialize value of type DataType from String float16".

No new codec is needed: the existing bytes codec handles any fixed-itemsize
data type. The work is the in-memory representation, because neither Java nor
ucar.ma2 has a 16-bit float. float16 is therefore held as ucar.ma2.FLOAT and
converted at the codec boundary, which makes it the first data type whose
encoded width (2 bytes) differs from its in-memory width (4 bytes).

- utils/Float16: IEEE 754 binary16 <-> float conversions. Float.float16ToFloat
  is Java 20+ and this project targets Java 8, so the bit manipulation is
  written out, covering subnormals, signed zeros, infinities, NaN payloads and
  round-half-to-even ties.
- core/DataType.isHalfPrecisionFloat(): default false, so the bytes codec can
  detect the width mismatch without importing the v2/v3 enums.
- core/codec/core/BytesCodec: widen on decode, narrow on encode. The float16
  branch precedes the ArrayFloat branch, since float16 is held in an ArrayFloat
  and must not be written as float32.
- core/ArrayMetadata.parseFillValue: accepts numbers, NaN, +/-Infinity, and the
  hex/binary forms. The 2 bytes of a hex fill value are a binary16 bit pattern,
  not a truncated float32, so they are widened rather than read as a float.
- v3/DataType.FLOAT16 ("float16"), v2/DataType.FLOAT16 ("<f2") and FLOAT16_BE
  (">f2"). v2 needed no byte-count change: getByteCount() parses the dtype
  string, giving 2 for f2. Blosc typesize follows getByteCount() and so becomes
  2, matching the encoded width.

Narrowing is lossy and documented as such: values at or above 65520.0 become
infinity, values below 2^-25 become signed zero, and everything else is rounded
to 11 significand bits. ZarrTest.testdata/assertIsTestdata now pre-quantize
float test values, because 1024 of the 4096 values they generate are not
exactly representable in binary16 (2049 stores as 2048, 4095 as 4096); the
quantized values match np.arange(4096, dtype='float16') element for element.

Verified against numpy 2.4.6 and zarr-python 3.1.6:
- 39 conversion cases asserted against numpy bit patterns, plus an exhaustive
  sweep proving all 65536 binary16 patterns round-trip (NaN stays NaN).
- Round-trips in both endiannesses, through blosc/zstd/gzip/sharding/crc32c,
  and for v2 and v3.
- Cross-implementation in both directions: zarr-python writes / zarr-java reads
  and zarr-java writes / zarr-python reads, for v2 and v3 and both byte orders.
  The chunk bytes are byte-identical to zarr-python's in all four
  configurations, including the partial trailing chunk.
- Confirmed the interop test has teeth: injecting a symmetric byte-swap into
  both encode and decode still passes the zarr-java round-trip, while failing
  all 8 interop cases.

Follow-ups, deliberately not attempted here:
- complex64/complex128: ucar.ma2 has no complex type, so the in-memory
  representation (trailing dimension of 2 vs. a custom ArrayObject) is a design
  decision for the maintainers rather than something to settle in this PR.
- r* (r8, r16, ...) and fixed_length_utf32: parameterized data types. r* needs
  its name parsed as a pattern and fixed_length_utf32 carries a configuration
  object, so data_type stops being a plain JSON string and the @jsonvalue enum
  cannot model either. That is a separate refactor.
- string and variable_length_bytes: these break the fixed-itemsize invariant of
  getByteCount() and belong with the vlen work.
- ZarrPythonTests still drives its data type matrix from the hand-maintained
  dataTypeProviderV3/V2 lists, which FLOAT16 has been added to. If those are
  switched to DataType.values(), note that zarr_python_write.py builds test
  data with np.arange, whose float16 values above 2048 are quantized -- the
  Java side already accounts for this via ZarrTest.quantize.
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