From 89e0cf2ba8916f0a956af4602700cd68fa6b4489 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Mon, 15 Jun 2026 23:11:00 +0200 Subject: [PATCH] feat(reader): lazy ALP-RD decode via top-level records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the eager ALP-RD expansion with two top-level record types in reader.array: LazyAlpRdDoubleArray and LazyAlpRdFloatArray. ALP-RD splits each f64/f32 into a u16 "left" dictionary code and a u64 (f64) or u32 (f32) "right" payload (the low rightBitWidth bits of the original IEEE-754 bit pattern). Reconstruction is: bits = (dict[left[i]] << rightBitWidth) | right[i] result = longBitsToDouble(bits) // or intBitsToFloat for f32 Optional patches override the dict lookup at specific absolute indices for outlier values that don't compress through the dictionary. getDouble(i) / getFloat(i) read left + right children, dispatch through dict (or patch via binary search on patchIndices when present), shift+or, then bits-to-double/float. No n-sized output buffer allocated. The decoder pulls the small (typically ≤ 16-entry) dict from metadata into a short[] as before; left + right children are kept as typed ShortArray + LongArray/IntArray (no full materialise); patches collapse to (patchIndices: Array, patchLeftValues: short[], offset: long). The patchIndices Array is whichever narrow-int type the encoder chose (U8/U16/U32/U64) — looked up via SparseArrays.findPatch which already centralises the unsigned-index binary search. Bool and VarBin are not in scope for ALP-RD; accepts() restricts to F32/F64. AlpRdEncodingEncoderTest converted to use typed DoubleArray.getDouble / FloatArray.getFloat instead of ArraySegments.of(_).get(LE_xx, off), which no longer works on lazy outputs. 3 new unit tests in LazyAlpRdArrayTest cover f64 dict reconstruction, f32 dict reconstruction, and the patches override path. ./mvnw verify green (13 modules, integration suite 48s). Co-Authored-By: Claude Opus 4.7 --- .../reader/array/LazyAlpRdDoubleArray.java | 72 +++++++++ .../reader/array/LazyAlpRdFloatArray.java | 55 +++++++ .../reader/decode/AlpRdEncodingDecoder.java | 128 ++++++---------- .../reader/array/LazyAlpRdArrayTest.java | 137 ++++++++++++++++++ .../encode/AlpRdEncodingEncoderTest.java | 4 +- 5 files changed, 309 insertions(+), 87 deletions(-) create mode 100644 reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdDoubleArray.java create mode 100644 reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdFloatArray.java create mode 100644 reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyAlpRdArrayTest.java diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdDoubleArray.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdDoubleArray.java new file mode 100644 index 00000000..5131e66c --- /dev/null +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdDoubleArray.java @@ -0,0 +1,72 @@ +package io.github.dfa1.vortex.reader.array; + +import io.github.dfa1.vortex.core.DType; + +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleConsumer; + +/// Lazy ALP-RD-encoded {@link DoubleArray}. +/// +/// ALP-RD splits each f64 into a "left" u16 dictionary code and a "right" u64 payload +/// (the low `rightBitWidth` bits of the original bit pattern). Reconstruction is +/// `Double.longBitsToDouble((dict[left[i]] << rightBitWidth) | right[i])`. A small +/// patches table overrides the dict lookup at specific absolute indices for outlier +/// values that don't compress through the dictionary. +/// +/// {@code getDouble(i)} resolves on demand: read left/right children, dispatch through +/// dict (or patch if `i + patchOffset` hits a patch index), shift+or, then bits-to-double. +/// +/// @param dtype logical element type +/// @param length total logical row count +/// @param dict small u16 dictionary lookup table (length typically ≤ 16) +/// @param rightBitWidth bit width of the right payload +/// @param leftArr per-row u16 dict codes +/// @param rightArr per-row u64 right payloads (raw bits) +/// @param patchIndices sorted absolute indices of patched rows (or {@code null} when no patches) +/// @param patchLeftValues actual left u16 values for patched rows; aligned with +/// {@code patchIndices} +/// @param patchOffset absolute origin subtracted from row positions before patch lookup +public record LazyAlpRdDoubleArray( + DType dtype, long length, + short[] dict, int rightBitWidth, + ShortArray leftArr, LongArray rightArr, + Array patchIndices, short[] patchLeftValues, long patchOffset) + implements DoubleArray { + + @Override + public double getDouble(long i) { + int leftU16 = lookupLeft(i); + long leftBits = ((long) (leftU16 & 0xFFFF)) << rightBitWidth; + long rightBits = rightArr.getLong(i); + return Double.longBitsToDouble(leftBits | rightBits); + } + + @Override + public void forEachDouble(DoubleConsumer c) { + long n = length; + for (long i = 0; i < n; i++) { + c.accept(getDouble(i)); + } + } + + @Override + public double fold(double identity, DoubleBinaryOperator op) { + double acc = identity; + long n = length; + for (long i = 0; i < n; i++) { + acc = op.applyAsDouble(acc, getDouble(i)); + } + return acc; + } + + private int lookupLeft(long i) { + if (patchIndices != null) { + int p = SparseArrays.findPatch(patchIndices, patchLeftValues.length, i + patchOffset); + if (p >= 0) { + return patchLeftValues[p]; + } + } + short code = leftArr.getShort(i); + return dict[Short.toUnsignedInt(code)]; + } +} diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdFloatArray.java b/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdFloatArray.java new file mode 100644 index 00000000..d28b7f24 --- /dev/null +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/array/LazyAlpRdFloatArray.java @@ -0,0 +1,55 @@ +package io.github.dfa1.vortex.reader.array; + +import io.github.dfa1.vortex.core.DType; + +import java.util.function.DoubleBinaryOperator; + +/// Lazy ALP-RD-encoded {@link FloatArray}. See {@link LazyAlpRdDoubleArray} for the +/// dict + right + patches reconstruction model. The right payload is u32 instead of +/// u64 and the result is reinterpreted via {@link Float#intBitsToFloat}. +/// +/// @param dtype logical element type +/// @param length total logical row count +/// @param dict small u16 dictionary lookup table +/// @param rightBitWidth bit width of the right payload +/// @param leftArr per-row u16 dict codes +/// @param rightArr per-row u32 right payloads (raw bits) +/// @param patchIndices sorted absolute indices of patched rows (or {@code null}) +/// @param patchLeftValues actual left u16 values for patched rows +/// @param patchOffset absolute origin subtracted from row positions before patch lookup +public record LazyAlpRdFloatArray( + DType dtype, long length, + short[] dict, int rightBitWidth, + ShortArray leftArr, IntArray rightArr, + Array patchIndices, short[] patchLeftValues, long patchOffset) + implements FloatArray { + + @Override + public float getFloat(long i) { + int leftU16 = lookupLeft(i); + int leftBits = (leftU16 & 0xFFFF) << rightBitWidth; + int rightBits = rightArr.getInt(i); + return Float.intBitsToFloat(leftBits | rightBits); + } + + @Override + public double fold(double identity, DoubleBinaryOperator op) { + double acc = identity; + long n = length; + for (long i = 0; i < n; i++) { + acc = op.applyAsDouble(acc, getFloat(i)); + } + return acc; + } + + private int lookupLeft(long i) { + if (patchIndices != null) { + int p = SparseArrays.findPatch(patchIndices, patchLeftValues.length, i + patchOffset); + if (p >= 0) { + return patchLeftValues[p]; + } + } + short code = leftArr.getShort(i); + return dict[Short.toUnsignedInt(code)]; + } +} diff --git a/reader/src/main/java/io/github/dfa1/vortex/reader/decode/AlpRdEncodingDecoder.java b/reader/src/main/java/io/github/dfa1/vortex/reader/decode/AlpRdEncodingDecoder.java index 2e30a237..4518c1e0 100644 --- a/reader/src/main/java/io/github/dfa1/vortex/reader/decode/AlpRdEncodingDecoder.java +++ b/reader/src/main/java/io/github/dfa1/vortex/reader/decode/AlpRdEncodingDecoder.java @@ -8,12 +8,15 @@ import io.github.dfa1.vortex.proto.ALPRDMetadata; import io.github.dfa1.vortex.proto.PatchesMetadata; import io.github.dfa1.vortex.reader.array.Array; -import io.github.dfa1.vortex.reader.array.MaterializedDoubleArray; -import io.github.dfa1.vortex.reader.array.MaterializedFloatArray; +import io.github.dfa1.vortex.reader.array.IntArray; +import io.github.dfa1.vortex.reader.array.LazyAlpRdDoubleArray; +import io.github.dfa1.vortex.reader.array.LazyAlpRdFloatArray; +import io.github.dfa1.vortex.reader.array.LongArray; +import io.github.dfa1.vortex.reader.array.MaskedArray; +import io.github.dfa1.vortex.reader.array.ShortArray; import java.io.IOException; import java.lang.foreign.MemorySegment; -import java.lang.foreign.ValueLayout; import java.nio.ByteBuffer; /// Read-only decoder for {@code vortex.alprd}. @@ -58,104 +61,61 @@ public Array decode(DecodeContext ctx) { long n = ctx.rowCount(); PType ptype = p.ptype(); + // Lazy path: keep left/right as typed Arrays + patches as a small short[] + + // a lazy indices Array. No n-sized output buffer allocated. + Array leftRaw = ctx.decodeChild(0, U16_DTYPE, n); + ShortArray leftArr = (ShortArray) unwrap(leftRaw); + + Patches patches = decodePatches(ctx, meta.patches()); + return switch (ptype) { - case F64 -> decodeF64(ctx, meta, dict, rightBitWidth, n); - case F32 -> decodeF32(ctx, meta, dict, rightBitWidth, n); + case F64 -> { + Array rightRaw = ctx.decodeChild(1, U64_DTYPE, n); + LongArray rightArr = (LongArray) unwrap(rightRaw); + yield new LazyAlpRdDoubleArray(ctx.dtype(), n, dict, rightBitWidth, + leftArr, rightArr, patches.indices, patches.leftValues, patches.offset); + } + case F32 -> { + Array rightRaw = ctx.decodeChild(1, U32_DTYPE, n); + IntArray rightArr = (IntArray) unwrap(rightRaw); + yield new LazyAlpRdFloatArray(ctx.dtype(), n, dict, rightBitWidth, + leftArr, rightArr, patches.indices, patches.leftValues, patches.offset); + } default -> throw new VortexException(EncodingId.VORTEX_ALPRD, "unsupported dtype " + ptype); }; } - private static Array decodeF64(DecodeContext ctx, ALPRDMetadata meta, short[] dict, int rightBitWidth, long n) { - MemorySegment leftSeg = ctx.decodeChildSegment(0, U16_DTYPE, n); - MemorySegment rightSeg = ctx.decodeChildSegment(1, U64_DTYPE, n); - long leftCap = SegmentBroadcast.capacity(leftSeg, 2); - long rightCap = SegmentBroadcast.capacity(rightSeg, 8); - MemorySegment out = ctx.arena().allocate(n * Long.BYTES, Long.BYTES); - - for (long i = 0; i < n; i++) { - int code = Short.toUnsignedInt(leftSeg.getAtIndex(PTypeIO.LE_SHORT, i % leftCap)); - long leftBits = (long) (dict[code] & 0xFFFF) << rightBitWidth; - long rightBits = rightSeg.getAtIndex(PTypeIO.LE_LONG, i % rightCap); - out.setAtIndex(PTypeIO.LE_LONG, i, leftBits | rightBits); - } - - if (meta.patches() != null) { - applyPatchesF64(ctx, meta.patches(), out, rightSeg, rightCap, rightBitWidth); - } - - return new MaterializedDoubleArray(ctx.dtype(), n, out.asReadOnly()); + private static Array unwrap(Array arr) { + return arr instanceof MaskedArray m ? m.inner() : arr; } - private static Array decodeF32(DecodeContext ctx, ALPRDMetadata meta, short[] dict, int rightBitWidth, long n) { - MemorySegment leftSeg = ctx.decodeChildSegment(0, U16_DTYPE, n); - MemorySegment rightSeg = ctx.decodeChildSegment(1, U32_DTYPE, n); - long leftCap = SegmentBroadcast.capacity(leftSeg, 2); - long rightCap = SegmentBroadcast.capacity(rightSeg, 4); - MemorySegment out = ctx.arena().allocate(n * Integer.BYTES, Integer.BYTES); - - for (long i = 0; i < n; i++) { - int code = Short.toUnsignedInt(leftSeg.getAtIndex(PTypeIO.LE_SHORT, i % leftCap)); - int leftBits = (dict[code] & 0xFFFF) << rightBitWidth; - int rightBits = rightSeg.getAtIndex(PTypeIO.LE_INT, i % rightCap); - out.setAtIndex(PTypeIO.LE_INT, i, leftBits | rightBits); - } - - if (meta.patches() != null) { - applyPatchesF32(ctx, meta.patches(), out, rightSeg, rightCap, rightBitWidth); - } - - return new MaterializedFloatArray(ctx.dtype(), n, out.asReadOnly()); + /// Decoded patches: sorted absolute indices (as a typed Array for in-place lookup) + /// plus the actual left u16 values pulled into a short[]. + private record Patches(Array indices, short[] leftValues, long offset) { + static final Patches EMPTY = new Patches(null, new short[0], 0L); } - private static void applyPatchesF64(DecodeContext ctx, PatchesMetadata pm, - MemorySegment out, MemorySegment rightSeg, long rightCap, int rightBitWidth) { - long numPatches = pm.len(); - long offset = pm.offset(); - PType idxPtype = PType.fromOrdinal(pm.indices_ptype().value()); - - MemorySegment idxSeg = ctx.decodeChildSegment(2, new DType.Primitive(idxPtype, false), numPatches); - MemorySegment valSeg = ctx.decodeChildSegment(3, U16_DTYPE, numPatches); - int idxBytes = idxPtype.byteSize(); - long valCap = SegmentBroadcast.capacity(valSeg, 2); - - for (long j = 0; j < numPatches; j++) { - long absIdx = readUnsigned(idxSeg, SegmentBroadcast.elementOffset(idxSeg, j, idxBytes), idxPtype) - offset; - short actualLeftU16 = valSeg.getAtIndex(PTypeIO.LE_SHORT, j % valCap); - long leftBits = (long) (actualLeftU16 & 0xFFFF) << rightBitWidth; - long rightBits = rightSeg.getAtIndex(PTypeIO.LE_LONG, absIdx % rightCap); - out.setAtIndex(PTypeIO.LE_LONG, absIdx, leftBits | rightBits); + private static Patches decodePatches(DecodeContext ctx, PatchesMetadata pm) { + if (pm == null || pm.len() == 0) { + return Patches.EMPTY; } - } - - private static void applyPatchesF32(DecodeContext ctx, PatchesMetadata pm, - MemorySegment out, MemorySegment rightSeg, long rightCap, int rightBitWidth) { long numPatches = pm.len(); long offset = pm.offset(); PType idxPtype = PType.fromOrdinal(pm.indices_ptype().value()); + DType idxDtype = new DType.Primitive(idxPtype, false); - MemorySegment idxSeg = ctx.decodeChildSegment(2, new DType.Primitive(idxPtype, false), numPatches); + Array idxArr = ctx.decodeChild(2, idxDtype, numPatches); + Array idxData = idxArr instanceof MaskedArray m ? m.inner() : idxArr; + + // Pull the small left-values table into a short[] so lookups don't pay an + // Array-dispatch per patch hit. Patches are typically <1% of rows. MemorySegment valSeg = ctx.decodeChildSegment(3, U16_DTYPE, numPatches); - int idxBytes = idxPtype.byteSize(); long valCap = SegmentBroadcast.capacity(valSeg, 2); - - for (long j = 0; j < numPatches; j++) { - long absIdx = readUnsigned(idxSeg, SegmentBroadcast.elementOffset(idxSeg, j, idxBytes), idxPtype) - offset; - short actualLeftU16 = valSeg.getAtIndex(PTypeIO.LE_SHORT, j % valCap); - int leftBits = (actualLeftU16 & 0xFFFF) << rightBitWidth; - int rightBits = rightSeg.getAtIndex(PTypeIO.LE_INT, absIdx % rightCap); - out.setAtIndex(PTypeIO.LE_INT, (int) absIdx, leftBits | rightBits); + short[] leftValues = new short[(int) numPatches]; + for (int j = 0; j < numPatches; j++) { + leftValues[j] = valSeg.getAtIndex(PTypeIO.LE_SHORT, j % valCap); } - } - - private static long readUnsigned(MemorySegment seg, long off, PType ptype) { - return switch (ptype) { - case U8 -> Byte.toUnsignedLong(seg.get(ValueLayout.JAVA_BYTE, off)); - case U16 -> Short.toUnsignedLong(seg.get(PTypeIO.LE_SHORT, off)); - case U32 -> Integer.toUnsignedLong(seg.get(PTypeIO.LE_INT, off)); - case U64 -> seg.get(PTypeIO.LE_LONG, off); - default -> throw new VortexException(EncodingId.VORTEX_ALPRD, - "non-unsigned patch index ptype " + ptype); - }; + return new Patches(idxData, leftValues, offset); } private static ALPRDMetadata parseMeta(DecodeContext ctx) { diff --git a/reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyAlpRdArrayTest.java b/reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyAlpRdArrayTest.java new file mode 100644 index 00000000..b36aead6 --- /dev/null +++ b/reader/src/test/java/io/github/dfa1/vortex/reader/array/LazyAlpRdArrayTest.java @@ -0,0 +1,137 @@ +package io.github.dfa1.vortex.reader.array; + +import io.github.dfa1.vortex.core.DType; +import io.github.dfa1.vortex.core.PType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import static org.assertj.core.api.Assertions.assertThat; + +/// Unit tests for the lazy ALP-RD records. Cover dict + right reconstruction for +/// both F64 and F32, and the patches path that overrides the dict lookup at +/// specific indices. +class LazyAlpRdArrayTest { + + private static final DType F64 = new DType.Primitive(PType.F64, false); + private static final DType F32 = new DType.Primitive(PType.F32, false); + + @Nested + class DoubleDispatch { + + @Test + void reconstructsBitsViaDict() { + // Given 3.14 has bit pattern with a particular left/right split. + // We pick rightBitWidth = 48 → left = top 16 bits. + try (Arena arena = Arena.ofConfined()) { + int rightBitWidth = 48; + double[] sample = {3.14, 2.71, -1.0}; + short[] dict = new short[sample.length]; + long[] rights = new long[sample.length]; + long rightMask = (1L << rightBitWidth) - 1; + for (int i = 0; i < sample.length; i++) { + long bits = Double.doubleToRawLongBits(sample[i]); + dict[i] = (short) ((bits >>> rightBitWidth) & 0xFFFF); + rights[i] = bits & rightMask; + } + // Each row uses code i. + ShortArray leftArr = shortArray(arena, (short) 0, (short) 1, (short) 2); + LongArray rightArr = longArray(arena, rights); + + // When + var sut = new LazyAlpRdDoubleArray(F64, sample.length, dict, rightBitWidth, + leftArr, rightArr, null, new short[0], 0L); + + // Then + for (int i = 0; i < sample.length; i++) { + assertThat(sut.getDouble(i)).as("index %d", i).isEqualTo(sample[i]); + } + } + } + + @Test + void patchOverridesDictAtPatchedIndex() { + // Given a single-entry dict for non-patched rows, plus a patch overriding row 1 + // with a different left value. + try (Arena arena = Arena.ofConfined()) { + int rightBitWidth = 48; + double base = 1.5; + double patched = -42.0; + long baseBits = Double.doubleToRawLongBits(base); + long patchedBits = Double.doubleToRawLongBits(patched); + long rightMask = (1L << rightBitWidth) - 1; + + short[] dict = {(short) ((baseBits >>> rightBitWidth) & 0xFFFF)}; + ShortArray leftArr = shortArray(arena, (short) 0, (short) 0, (short) 0); + LongArray rightArr = longArray(arena, + baseBits & rightMask, patchedBits & rightMask, baseBits & rightMask); + Array patchIdx = intArray(arena, 1); + short[] patchLeft = {(short) ((patchedBits >>> rightBitWidth) & 0xFFFF)}; + + // When + var sut = new LazyAlpRdDoubleArray(F64, 3, dict, rightBitWidth, + leftArr, rightArr, patchIdx, patchLeft, 0L); + + // Then + assertThat(sut.getDouble(0)).isEqualTo(base); + assertThat(sut.getDouble(1)).isEqualTo(patched); + assertThat(sut.getDouble(2)).isEqualTo(base); + } + } + } + + @Nested + class FloatDispatch { + + @Test + void reconstructsBitsViaDict() { + try (Arena arena = Arena.ofConfined()) { + int rightBitWidth = 16; + float[] sample = {1.5f, -2.25f}; + short[] dict = new short[sample.length]; + int[] rights = new int[sample.length]; + int rightMask = (1 << rightBitWidth) - 1; + for (int i = 0; i < sample.length; i++) { + int bits = Float.floatToRawIntBits(sample[i]); + dict[i] = (short) ((bits >>> rightBitWidth) & 0xFFFF); + rights[i] = bits & rightMask; + } + ShortArray leftArr = shortArray(arena, (short) 0, (short) 1); + IntArray rightArr = intArray(arena, rights); + + var sut = new LazyAlpRdFloatArray(F32, sample.length, dict, rightBitWidth, + leftArr, rightArr, null, new short[0], 0L); + + assertThat(sut.getFloat(0)).isEqualTo(1.5f); + assertThat(sut.getFloat(1)).isEqualTo(-2.25f); + } + } + } + + private static ShortArray shortArray(Arena arena, short... vs) { + MemorySegment seg = arena.allocate(vs.length * 2L, 2); + for (int i = 0; i < vs.length; i++) { + seg.setAtIndex(ValueLayout.JAVA_SHORT, i, vs[i]); + } + return new MaterializedShortArray(new DType.Primitive(PType.U16, false), vs.length, seg.asReadOnly()); + } + + private static IntArray intArray(Arena arena, int... vs) { + MemorySegment seg = arena.allocate(vs.length * 4L, 4); + for (int i = 0; i < vs.length; i++) { + seg.setAtIndex(ValueLayout.JAVA_INT, i, vs[i]); + } + return new MaterializedIntArray(new DType.Primitive(PType.I32, false), vs.length, seg.asReadOnly()); + } + + private static LongArray longArray(Arena arena, long... vs) { + MemorySegment seg = arena.allocate(vs.length * 8L, 8); + for (int i = 0; i < vs.length; i++) { + seg.setAtIndex(ValueLayout.JAVA_LONG, i, vs[i]); + } + return new MaterializedLongArray(new DType.Primitive(PType.I64, false), vs.length, seg.asReadOnly()); + } +} diff --git a/writer/src/test/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoderTest.java b/writer/src/test/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoderTest.java index 17c34dd8..cc66d1c7 100644 --- a/writer/src/test/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoderTest.java +++ b/writer/src/test/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoderTest.java @@ -1,10 +1,8 @@ package io.github.dfa1.vortex.writer.encode; -import io.github.dfa1.vortex.reader.array.ArraySegments; import io.github.dfa1.vortex.encoding.DTypes; import io.github.dfa1.vortex.reader.decode.DecodeContext; -import io.github.dfa1.vortex.encoding.PTypeIO; import io.github.dfa1.vortex.reader.ReadRegistry; import io.github.dfa1.vortex.reader.decode.TestRegistry; import io.github.dfa1.vortex.proto.ALPRDMetadata; @@ -33,7 +31,7 @@ void encode_f64_roundTrip() { // Then for (int i = 0; i < values.length; i++) { - assertThat(ArraySegments.of(result).get(PTypeIO.LE_DOUBLE, (long) i * 8)) + assertThat(((io.github.dfa1.vortex.reader.array.DoubleArray) result).getDouble(i)) .as("index %d", i).isCloseTo(values[i], within(1e-9)); } }