diff --git a/src/main/java/dev/zarr/zarrjava/utils/IndexingUtils.java b/src/main/java/dev/zarr/zarrjava/utils/IndexingUtils.java index b108aca..9ff0764 100644 --- a/src/main/java/dev/zarr/zarrjava/utils/IndexingUtils.java +++ b/src/main/java/dev/zarr/zarrjava/utils/IndexingUtils.java @@ -87,7 +87,7 @@ public static ChunkProjection computeProjection( if (selOffset[dimIdx] + selShape[dimIdx] > dimLimit) { // selection ends after current chunk - shape[dimIdx] = (int) (chunkShape[dimIdx] - (selOffset[dimIdx] % chunkShape[dimIdx])); + shape[dimIdx] = chunkShape[dimIdx] - chunkOffset[dimIdx]; } else { // selection ends within current chunk shape[dimIdx] = (int) (selOffset[dimIdx] + selShape[dimIdx] - dimOffset @@ -187,5 +187,15 @@ public ChunkProjection( this.outOffset = outOffset; this.shape = shape; } + + @Override + public String toString() { + return "ChunkProjection{" + + "chunkCoords=" + Arrays.toString(chunkCoords) + + ", chunkOffset=" + Arrays.toString(chunkOffset) + + ", outOffset=" + Arrays.toString(outOffset) + + ", shape=" + Arrays.toString(shape) + + '}'; + } } } \ No newline at end of file diff --git a/src/test/java/dev/zarr/zarrjava/TestUtils.java b/src/test/java/dev/zarr/zarrjava/TestUtils.java index 2a49f6b..582dfab 100644 --- a/src/test/java/dev/zarr/zarrjava/TestUtils.java +++ b/src/test/java/dev/zarr/zarrjava/TestUtils.java @@ -1,11 +1,13 @@ package dev.zarr.zarrjava; +import dev.zarr.zarrjava.utils.IndexingUtils; import org.junit.Test; import org.junit.jupiter.api.Assertions; import java.util.Arrays; +import static dev.zarr.zarrjava.utils.IndexingUtils.computeChunkCoords; import static dev.zarr.zarrjava.utils.Utils.inversePermutation; import static dev.zarr.zarrjava.utils.Utils.isPermutation; @@ -27,5 +29,52 @@ public void testInversePermutation() { Assertions.assertFalse(Arrays.equals(new int[]{2, 0, 1}, inversePermutation(new int[]{2, 0, 1}))); } + @Test + public void testComputeChunkCoords(){ + long[] arrayShape = new long[]{100, 100}; + int[] chunkShape = new int[]{30, 30}; + long[] selOffset = new long[]{50, 20}; + int[] selShape = new int[]{20, 1}; + long[][] chunkCoords = computeChunkCoords(arrayShape, chunkShape, selOffset, selShape); + long[][] expectedChunkCoords = new long[][]{ + {1, 0}, + {2, 0}, + }; + Assertions.assertArrayEquals(expectedChunkCoords, chunkCoords); + + arrayShape = new long[]{1, 52}; + chunkShape = new int[]{1, 17}; + selOffset = new long[]{0, 32}; + selShape = new int[]{1, 20}; + chunkCoords = computeChunkCoords(arrayShape, chunkShape, selOffset, selShape); + expectedChunkCoords = new long[][]{ + {0, 1}, + {0, 2}, + {0, 3}, + }; + Assertions.assertArrayEquals(expectedChunkCoords, chunkCoords); + } + + @Test + public void testComputeProjection(){ + // chunk (0,2) contains indexes 34-50 along axis 1 + // thus the overlap with selection 32-52 is 34-50 + // which is offset 2 in the selection and offset 0 in the chunk + // and has full chunk length 17 + final long[] chunkCoords = new long[]{0, 2}; + final long[] arrayShape = new long[]{1, 52}; + final int[] chunkShape = new int[]{1, 17}; + final long[] selOffset = new long[]{0, 32}; + final int[] selShape = new int[]{1, 20}; + + IndexingUtils.ChunkProjection projection = IndexingUtils.computeProjection( + chunkCoords, arrayShape, chunkShape, selOffset, selShape + ); + Assertions.assertArrayEquals(chunkCoords, projection.chunkCoords); + Assertions.assertArrayEquals(new int[]{0,0}, projection.chunkOffset); + Assertions.assertArrayEquals(new int[]{0,2}, projection.outOffset); + Assertions.assertArrayEquals(new int[]{1, 17}, projection.shape); + } + } diff --git a/src/test/java/dev/zarr/zarrjava/ZarrV3Test.java b/src/test/java/dev/zarr/zarrjava/ZarrV3Test.java index 9a669be..8390c3c 100644 --- a/src/test/java/dev/zarr/zarrjava/ZarrV3Test.java +++ b/src/test/java/dev/zarr/zarrjava/ZarrV3Test.java @@ -6,6 +6,7 @@ import dev.zarr.zarrjava.core.Attributes; import dev.zarr.zarrjava.store.FilesystemStore; import dev.zarr.zarrjava.store.HttpStore; +import dev.zarr.zarrjava.store.MemoryStore; import dev.zarr.zarrjava.store.StoreHandle; import dev.zarr.zarrjava.utils.MultiArrayUtils; import dev.zarr.zarrjava.v3.*; @@ -106,6 +107,17 @@ static Stream> chunkKeyEnco return Stream.concat(builders, codecBuilders().map(codecFunc -> b -> b.withCodecs(codecFunc))); } + static Stream unalignedArrayAccessProvider() { + Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(52, 17, 32)); + builder.add(Arguments.of(71, 11, 12)); + builder.add(Arguments.of(52, 17, 17)); + builder.add(Arguments.of(50, 3, 7)); + builder.add(Arguments.of(50, 3, 22)); + builder.add(Arguments.of(13, 31, 21)); + return builder.build(); + } + @ParameterizedTest @MethodSource("invalidCodecBuilder") public void testCheckInvalidCodecConfiguration(Function codecBuilder) { @@ -735,4 +747,30 @@ public void testGroupAttributes() throws IOException, ZarrException { group = Group.open(storeHandle); Assertions.assertEquals("group_value", group.metadata().attributes().getString("group_attr")); } -} + + @ParameterizedTest + @MethodSource("unalignedArrayAccessProvider") + public void testUnalignedArrayAccess(int arrayShape, int chunkShape, int accessShape) throws ZarrException, IOException { + Array array = Array.create( + new MemoryStore().resolve(), + Array.metadataBuilder() + .withShape(arrayShape) + .withDataType(DataType.UINT32) + .withChunkShape(chunkShape) + .withFillValue(0) + .build() + ); + + int[] testData = new int[arrayShape]; + Arrays.setAll(testData, p -> (byte) p); + ucar.ma2.Array data = ucar.ma2.Array.factory(ucar.ma2.DataType.UINT, new int[]{arrayShape}, testData); + array.write(data); + + for (int i = 0; i < arrayShape; i += accessShape) { + accessShape = Math.min(accessShape, arrayShape - i); + ucar.ma2.Array result = array.read(new long[]{i}, new int[]{accessShape}); + int[] expectedData = Arrays.copyOfRange(testData, i, i + accessShape); + Assertions.assertArrayEquals(expectedData, (int[]) result.get1DJavaArray(ucar.ma2.DataType.UINT)); + } + } +} \ No newline at end of file