diff --git a/pom.xml b/pom.xml index 4da66c389..80e778aa4 100644 --- a/pom.xml +++ b/pom.xml @@ -175,6 +175,10 @@ commons-io commons-io + + commons-codec + commons-codec + diff --git a/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java b/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java index a69be3503..9484adb78 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java @@ -31,6 +31,7 @@ import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; @@ -52,6 +53,8 @@ import java.util.stream.Collectors; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; +import org.janelia.saalfeldlab.n5.codec.DatasetCodec; +import org.janelia.saalfeldlab.n5.codec.DatasetCodecInfo; /** @@ -95,8 +98,11 @@ public class DatasetAttributes implements Serializable { private final DataType dataType; + private final JsonElement defaultValue; + private final BlockCodecInfo blockCodecInfo; private final DataCodecInfo[] dataCodecInfos; + private final DatasetCodecInfo[] datasetCodecInfos; private transient final DatasetAccess access; @@ -104,14 +110,18 @@ public DatasetAttributes( final long[] dimensions, final int[] outerBlockSize, final DataType dataType, + final JsonElement defaultValue, final BlockCodecInfo blockCodecInfo, + final DatasetCodecInfo[] datasetCodecInfos, final DataCodecInfo... dataCodecInfos) { this.dimensions = dimensions; this.dataType = dataType; this.outerBlockSize = outerBlockSize; + this.defaultValue = defaultValue == null ? JsonNull.INSTANCE : defaultValue; this.blockCodecInfo = blockCodecInfo == null ? defaultBlockCodecInfo() : blockCodecInfo; + this.datasetCodecInfos = datasetCodecInfos; if (dataCodecInfos == null) this.dataCodecInfos = new DataCodecInfo[0]; @@ -124,6 +134,28 @@ public DatasetAttributes( blockSize = access.getGrid().getBlockSize(0); } + public DatasetAttributes( + final long[] dimensions, + final int[] outerBlockSize, + final DataType dataType, + final BlockCodecInfo blockCodecInfo, + final DatasetCodecInfo[] datasetCodecInfos, + final DataCodecInfo... dataCodecInfos) { + + this(dimensions, outerBlockSize, dataType, JsonNull.INSTANCE, + blockCodecInfo, datasetCodecInfos, dataCodecInfos); + } + + public DatasetAttributes( + final long[] dimensions, + final int[] outerBlockSize, + final DataType dataType, + final BlockCodecInfo blockCodecInfo, + final DataCodecInfo... dataCodecInfos) { + + this(dimensions, outerBlockSize, dataType, blockCodecInfo, null, dataCodecInfos); + } + /** * Constructs a DatasetAttributes instance with specified dimensions, block size, data type, * and single compressor with default codec. @@ -190,7 +222,16 @@ protected DatasetAccess createDatasetAccess() { } } - return new DefaultDatasetAccess<>(grid, blockCodecs); + final DatasetCodec[] datasetCodecs; + if (datasetCodecInfos != null) { + datasetCodecs = new DatasetCodec[datasetCodecInfos.length]; + for (int i = 0; i < datasetCodecInfos.length; i++) + datasetCodecs[i] = datasetCodecInfos[i].create(this); + + } else + datasetCodecs = new DatasetCodec[0]; + + return new DefaultDatasetAccess<>(grid, blockCodecs, datasetCodecs); } private static int nestingDepth(BlockCodecInfo info) { @@ -202,7 +243,6 @@ private static int nestingDepth(BlockCodecInfo info) { } } - protected BlockCodecInfo defaultBlockCodecInfo() { return new N5BlockCodecInfo(); @@ -223,6 +263,11 @@ public int[] getBlockSize() { return blockSize; } + public JsonElement getDefaultValue() { + + return defaultValue; + } + public boolean isSharded() { return blockCodecInfo instanceof ShardCodecInfo; @@ -284,6 +329,11 @@ public DataCodecInfo[] getDataCodecInfos() { return dataCodecInfos; } + public DatasetCodecInfo[] getDatasetCodecInfos() { + + return datasetCodecInfos; + } + public String relativeBlockPath(long... position) { return Arrays.stream(position).mapToObj(Long::toString).collect(Collectors.joining("/")); diff --git a/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java b/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java index 0a5be6cd1..8d3008ae0 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java @@ -276,6 +276,7 @@ default void writeBlock( "Failed to write block " + Arrays.toString(dataBlock.getGridPosition()) + " into dataset " + path, e); } + } @Override @@ -300,4 +301,4 @@ default boolean deleteBlock( final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(path), datasetAttributes); return datasetAttributes.getDatasetAccess().deleteBlock(posKva, gridPosition); } -} +} \ No newline at end of file diff --git a/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java b/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java index 3f15be8f9..cc9bc7646 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java @@ -28,6 +28,7 @@ */ package org.janelia.saalfeldlab.n5; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; @@ -196,39 +197,46 @@ default boolean remove() throws N5Exception { /** * Creates a dataset. This does not create any data but the path and - * mandatory attributes only. + * mandatory attributes only. The returned DatasetAttributes should be used + * for future read/write operations on this dataset. It may not be the same + * DatasetAttributes object that was provided, depending on the implementation. * * @param datasetPath dataset path * @param datasetAttributes the dataset attributes - * @throws N5Exception the exception + * @return DatasetAttributes optimal attributes object to be used for read/write operations + * @throws N5Exception */ - default void createDataset( + default DatasetAttributes createDataset( final String datasetPath, final DatasetAttributes datasetAttributes) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(datasetPath); createGroup(normalPath); setDatasetAttributes(normalPath, datasetAttributes); + return datasetAttributes; } /** * Creates a dataset. This does not create any data but the path and - * mandatory attributes only. + * mandatory attributes only. Returns the DatasetAttributes object to be + * used for future read/write operations on this dataset. * * @param datasetPath dataset path * @param dimensions the dataset dimensions * @param blockSize the block size * @param dataType the data type * @param compression the compression + * @return DatasetAttributes optimal attributes object to be used for read/write operations + * @throws N5Exception */ - default void createDataset( + default DatasetAttributes createDataset( final String datasetPath, final long[] dimensions, final int[] blockSize, final DataType dataType, final Compression compression) throws N5Exception { - createDataset(datasetPath, new DatasetAttributes(dimensions, blockSize, dataType, compression)); + return createDataset(datasetPath, new DatasetAttributes(dimensions, blockSize, dataType, compression)); } /** diff --git a/src/main/java/org/janelia/saalfeldlab/n5/StringDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/StringDataBlock.java index 77fd10eb4..fe846ebd6 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/StringDataBlock.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/StringDataBlock.java @@ -1,31 +1,3 @@ -/*- - * #%L - * Not HDF5 - * %% - * Copyright (C) 2017 - 2025 Stephan Saalfeld - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ /** * Copyright (c) 2017, Stephan Saalfeld * All rights reserved. diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodec.java new file mode 100644 index 000000000..dee1ba8ba --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodec.java @@ -0,0 +1,21 @@ +package org.janelia.saalfeldlab.n5.codec; + +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; + +/** + * A Codec that transforms the contents of a {@link DataBlock}. + *

+ * This class is N5's analogue to Zarr's array -> array codec. + */ +public interface DatasetCodec { + + // TODO Name ideas: + // "ImageCodec"? + // BlockTransformationCodec + + DataBlock encode(DataBlock block) throws N5IOException; + + DataBlock decode(DataBlock dataBlock) throws N5IOException; + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecInfo.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecInfo.java new file mode 100644 index 000000000..8ca2f40ad --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecInfo.java @@ -0,0 +1,38 @@ +/*- + * #%L + * Not HDF5 + * %% + * Copyright (C) 2017 - 2025 Stephan Saalfeld + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.janelia.saalfeldlab.n5.codec; + +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.serialization.NameConfig; + +@NameConfig.Prefix("data-codec") +public interface DatasetCodecInfo extends CodecInfo { + + DatasetCodec create(final DatasetAttributes attributes); +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBlockCodecInfo.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBlockCodecInfo.java index 1b0e848ef..4d4b981ff 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBlockCodecInfo.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBlockCodecInfo.java @@ -40,13 +40,12 @@ import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.serialization.NameConfig; - @NameConfig.Name(value = RawBlockCodecInfo.TYPE) public class RawBlockCodecInfo implements BlockCodecInfo { private static final long serialVersionUID = 3282569607795127005L; - public static final String TYPE = "bytes"; + public static final String TYPE = "raw-bytes"; @NameConfig.Parameter(value = "endian", optional = true) private final ByteOrder byteOrder; diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java index 2e5aa9817..f82d27054 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java @@ -29,40 +29,53 @@ package org.janelia.saalfeldlab.n5.codec.checksum; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.util.zip.CheckedInputStream; +import java.nio.ByteOrder; +import java.util.function.Supplier; import java.util.zip.CheckedOutputStream; import java.util.zip.Checksum; +import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.codec.CodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodec; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; -import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodecInfo; +import org.janelia.saalfeldlab.n5.codec.DeterministicSizeDataCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** - * A {@link CodecInfo} that appends a checksum to data when encoding and can validate against that checksum when decoding. + * A {@link CodecInfo} that appends a checksum to data when encoding and can + * validate against that checksum when decoding. + *

+ * Checksum codec instances are expected to be thread safe, but {@link Checksum} + * implementations may not be. As a result, subclasses of this implementation + * provide a {@link Supplier} for an appropriate Checksum type, a new instance + * of which is created by {@link #getChecksum()} for each + * {@link #encode(ReadData)} and {@link #decode(ReadData)} call. */ -public abstract class ChecksumCodec implements DataCodec, DataCodecInfo, DeterministicSizeCodecInfo { +public abstract class ChecksumCodec implements DataCodec, DataCodecInfo, DeterministicSizeDataCodec { private static final long serialVersionUID = 3141427377277375077L; private int numChecksumBytes; - private Checksum checksum; + private Supplier checksumSupplier; - public ChecksumCodec(Checksum checksum, int numChecksumBytes) { + public ChecksumCodec(Supplier checksumSupplier, int numChecksumBytes) { - this.checksum = checksum; + this.checksumSupplier = checksumSupplier; this.numChecksumBytes = numChecksumBytes; } + /** + * Returns a new {@link Checksum} instance. + * + * @return the checksum + */ public Checksum getChecksum() { - return checksum; + return checksumSupplier.get(); } public int numChecksumBytes() { @@ -71,13 +84,14 @@ public int numChecksumBytes() { } private CheckedOutputStream createStream(OutputStream out) { - return new CheckedOutputStream(out, getChecksum()) { + final Checksum checksum = getChecksum(); + return new CheckedOutputStream(out, checksum) { private boolean closed = false; - @Override public void close() throws IOException { - + @Override + public void close() throws IOException { if (!closed) { - writeChecksum(out); + writeChecksum(checksum, out); closed = true; out.close(); } @@ -88,37 +102,48 @@ private CheckedOutputStream createStream(OutputStream out) { @Override public ReadData encode(ReadData readData) { return readData.encode(this::createStream); - } @Override public ReadData decode(ReadData readData) throws N5IOException { + final ReadData rdm = readData.materialize(); + final long N = rdm.requireLength(); - return ReadData.from(new CheckedInputStream(readData.inputStream(), getChecksum())); - } + final ReadData data = rdm.slice(0, N - numChecksumBytes); + final long calculatedChecksum = computeChecksum(data); - @Override - public long encodedSize(final long size) { + final ReadData checksumRd = rdm.slice(N - numChecksumBytes, numChecksumBytes); + final long storedChecksum = readChecksum(checksumRd); - return size + numChecksumBytes(); + if( calculatedChecksum != storedChecksum) + throw new N5Exception(String.format("Calculated checksum (%d) does not match stored checksum (%d).", + calculatedChecksum, storedChecksum)); + + return data; } @Override - public long decodedSize(final long size) { + public long encodedSize(final long size) { - return size - numChecksumBytes(); + return size + numChecksumBytes(); } - protected boolean valid(InputStream in) throws IOException { + protected long readChecksum(ReadData checksumData) { - return readChecksum(in) == getChecksum().getValue(); + // the computed checksum is a long that can take values in [0, 2^32 - 1] + // so convert the four bytes to an appropriate long + ByteBuffer buf = ByteBuffer.allocate(8); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(checksumData.allBytes()); + buf.putInt(0); + buf.rewind(); + return buf.getLong(); } - protected long readChecksum(InputStream in) throws IOException { - - final byte[] checksum = new byte[numChecksumBytes()]; - in.read(checksum); - return ByteBuffer.wrap(checksum).getLong(); + protected long computeChecksum(ReadData data) { + final Checksum checksum = getChecksum(); + checksum.update(data.allBytes(), 0, (int)data.requireLength()); + return checksum.getValue(); } /** @@ -126,12 +151,11 @@ protected long readChecksum(InputStream in) throws IOException { * * @return a ByteBuffer representing the checksum value */ - public abstract ByteBuffer getChecksumValue(); + public abstract ByteBuffer getChecksumValue(Checksum checksum); - public void writeChecksum(OutputStream out) throws IOException { + protected void writeChecksum(Checksum checksum, OutputStream out) throws IOException { - out.write(getChecksumValue().array()); + out.write(getChecksumValue(checksum).array()); } - } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java index 678e14e8c..43c1a0203 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java @@ -28,12 +28,13 @@ */ package org.janelia.saalfeldlab.n5.codec.checksum; -import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.codec.digest.PureJavaCrc32C; import org.janelia.saalfeldlab.n5.codec.DataCodec; import org.janelia.saalfeldlab.n5.serialization.NameConfig; import java.nio.ByteBuffer; -import java.util.zip.CRC32; +import java.nio.ByteOrder; +import java.util.zip.Checksum; @NameConfig.Name(Crc32cChecksumCodec.TYPE) public class Crc32cChecksumCodec extends ChecksumCodec { @@ -44,26 +45,14 @@ public class Crc32cChecksumCodec extends ChecksumCodec { public Crc32cChecksumCodec() { - super(new CRC32(), 4); + super(() -> new PureJavaCrc32C(), 4); } @Override - public long encodedSize(final long size) { - - return size + numChecksumBytes(); - } - - @Override - public long decodedSize(final long size) { - - return size - numChecksumBytes(); - } - - @Override - public ByteBuffer getChecksumValue() { + public ByteBuffer getChecksumValue(Checksum checksum) { final ByteBuffer buf = ByteBuffer.allocate(numChecksumBytes()); - buf.putInt((int)getChecksum().getValue()); + buf.order(ByteOrder.LITTLE_ENDIAN).putInt((int)checksum.getValue()); return buf; } @@ -77,4 +66,5 @@ public String getType() { return this; } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/Transpose.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/Transpose.java new file mode 100644 index 000000000..90ccc0ff9 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/Transpose.java @@ -0,0 +1,240 @@ +package org.janelia.saalfeldlab.n5.codec.transpose; + +import org.janelia.saalfeldlab.n5.DataType; + +class Transpose { + + // TODO: detect when 1-sized dimensions are permuted. This should allow to + // simplify (or completely avoid) copying under certain conditions. + + public static int[] encode(final int[] decodedPos, final int[] order) { + final int[] encodedPos = new int[decodedPos.length]; + encode(decodedPos, order, encodedPos); + return encodedPos; + } + + public static int[] decode(final int[] encodedPos, final int[] order) { + final int[] decodedPos = new int[encodedPos.length]; + decode(encodedPos, order, decodedPos); + return decodedPos; + } + + public static void encode(int[] decodedPos, int[] order, int[] encodedPos) { + for (int d = 0; d < order.length; d++) + encodedPos[d] = decodedPos[order[d]]; + } + + public static void decode(int[] encodedPos, int[] order, int[] decodedPos) { + for (int d = 0; d < order.length; d++) + decodedPos[order[d]] = encodedPos[d]; + } + + @SuppressWarnings("unchecked") + public static Transpose of(final DataType dataType, final int numDimensions) { + return (Transpose) new Transpose<>(MemCopy.forDataType(dataType), numDimensions); + } + + private final MemCopy memCopy; + + private final int[] ssize; + private final int[] tsize; + private final int[] ssteps; + private final int[] tsteps; + private final int[] csteps; + + Transpose(final MemCopy memCopy, final int n) { + this.memCopy = memCopy; + ssize = new int[n]; + tsize = new int[n]; + ssteps = new int[n]; + tsteps = new int[n]; + csteps = new int[n]; + } + + public void encode(final T decoded, final T encoded, final int[] decodedSize, final int[] order) { + final int n = ssize.length; + + for (int d = 0; d < n; ++d) + ssize[d] = decodedSize[d]; + + for (int d = 0; d < n; ++d) + tsize[d] = decodedSize[order[d]]; + + ssteps[0] = 1; + for (int d = 0; d < n - 1; ++d) + ssteps[d + 1] = ssteps[d] * ssize[d]; + + tsteps[0] = 1; + for (int d = 0; d < n - 1; ++d) + tsteps[d + 1] = tsteps[d] * tsize[d]; + + for (int d = 0; d < n; ++d) + csteps[order[d]] = tsteps[d]; + + copyRecursively(decoded, 0, encoded, 0, n - 1); + } + + public void decode(final T encoded, final T decoded, final int[] decodedSize, final int[] order) { + final int n = ssize.length; + + for (int d = 0; d < n; ++d) + ssize[d] = decodedSize[order[d]]; + + ssteps[0] = 1; + for (int d = 0; d < n - 1; ++d) + ssteps[d + 1] = ssteps[d] * ssize[d]; + + tsteps[0] = 1; + for (int d = 0; d < n - 1; ++d) + tsteps[d + 1] = tsteps[d] * decodedSize[d]; + + for (int d = 0; d < n; ++d) + csteps[d] = tsteps[order[d]]; + + copyRecursively(encoded, 0, decoded, 0, n - 1); + } + + private void copyRecursively(final T src, final int srcPos, final T dest, final int destPos, final int d) { + if (d == 0) { + final int length = ssize[d]; + final int stride = csteps[d]; + memCopy.copyStrided(src, srcPos, dest, destPos, stride, length); + } else { + final int length = ssize[d]; + final int srcStride = ssteps[d]; + final int destStride = csteps[d]; + for (int i = 0; i < length; ++i) + copyRecursively(src, srcPos + i * srcStride, dest, destPos + i * destStride, d - 1); + } + } + + Transpose newInstance() { + return new Transpose<>(memCopy, ssize.length); + } + + /** + * Low-level range copying methods between source and target primitve array + * (type {@code T}, e.g., {@code double[]}). + * + * @param + * the source/target type. Must be a primitive array type (e.g., {@code double[]}) + */ + interface MemCopy { + + MemCopy.MemCopyByte BYTE = new MemCopy.MemCopyByte(); + MemCopy.MemCopyShort SHORT = new MemCopy.MemCopyShort(); + MemCopy.MemCopyInt INT = new MemCopy.MemCopyInt(); + MemCopy.MemCopyLong LONG = new MemCopy.MemCopyLong(); + MemCopy.MemCopyFloat FLOAT = new MemCopy.MemCopyFloat(); + MemCopy.MemCopyDouble DOUBLE = new MemCopy.MemCopyDouble(); + + static MemCopy forDataType(final DataType dataType) { + switch (dataType) { + case UINT8: + case INT8: + return BYTE; + case UINT16: + case INT16: + return SHORT; + case UINT32: + case INT32: + return INT; + case UINT64: + case INT64: + return LONG; + case FLOAT32: + return FLOAT; + case FLOAT64: + return DOUBLE; + case STRING: + case OBJECT: + throw new UnsupportedOperationException("TODO?"); + default: + throw new IllegalArgumentException(); + } + } + + /** + * Copy {@code length} components from the {@code src} array to the {@code + * dest} array. The components at positions {@code srcPos} through {@code + * srcPos+length-1} in the source array are copied into positions {@code + * destPos}, {@code destPos+destStride}, {@code destPos + 2*destStride}, + * etc., through {@code destPos+(length-1)*destStride} of the destination + * array. + */ + void copyStrided(T src, int srcPos, T dest, int destPos, int destStride, int length); + + class MemCopyByte implements MemCopy { + + @Override + public void copyStrided(final byte[] src, final int srcPos, final byte[] dest, final int destPos, final int destStride, final int length) { + if (destStride == 1) + System.arraycopy(src, srcPos, dest, destPos, length); + else + for (int i = 0; i < length; ++i) + dest[destPos + i * destStride] = src[srcPos + i]; + } + } + + class MemCopyShort implements MemCopy { + + @Override + public void copyStrided(final short[] src, final int srcPos, final short[] dest, final int destPos, final int destStride, final int length) { + if (destStride == 1) + System.arraycopy(src, srcPos, dest, destPos, length); + else + for (int i = 0; i < length; ++i) + dest[destPos + i * destStride] = src[srcPos + i]; + } + } + + class MemCopyInt implements MemCopy { + + @Override + public void copyStrided(final int[] src, final int srcPos, final int[] dest, final int destPos, final int destStride, final int length) { + if (destStride == 1) + System.arraycopy(src, srcPos, dest, destPos, length); + else + for (int i = 0; i < length; ++i) + dest[destPos + i * destStride] = src[srcPos + i]; + } + } + + class MemCopyLong implements MemCopy { + + @Override + public void copyStrided(final long[] src, final int srcPos, final long[] dest, final int destPos, final int destStride, final int length) { + if (destStride == 1) + System.arraycopy(src, srcPos, dest, destPos, length); + else + for (int i = 0; i < length; ++i) + dest[destPos + i * destStride] = src[srcPos + i]; + } + } + + class MemCopyFloat implements MemCopy { + + @Override + public void copyStrided(final float[] src, final int srcPos, final float[] dest, final int destPos, final int destStride, final int length) { + if (destStride == 1) + System.arraycopy(src, srcPos, dest, destPos, length); + else + for (int i = 0; i < length; ++i) + dest[destPos + i * destStride] = src[srcPos + i]; + } + } + + class MemCopyDouble implements MemCopy { + + @Override + public void copyStrided(final double[] src, final int srcPos, final double[] dest, final int destPos, final int destStride, final int length) { + if (destStride == 1) + System.arraycopy(src, srcPos, dest, destPos, length); + else + for (int i = 0; i < length; ++i) + dest[destPos + i * destStride] = src[srcPos + i]; + } + } + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodec.java new file mode 100644 index 000000000..b6de0a041 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodec.java @@ -0,0 +1,107 @@ +package org.janelia.saalfeldlab.n5.codec.transpose; + +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.codec.DatasetCodec; + +public class TransposeCodec implements DatasetCodec { + + private DataType dataType; + private final int[] order; + + private final Transpose transpose; + + public TransposeCodec(DataType dataType, int[] order) { + + this.order = order; + this.dataType = dataType; + transpose = Transpose.of(dataType, order.length); + } + + @SuppressWarnings("unchecked") + @Override + public DataBlock encode(DataBlock dataBlock) { + + DataBlock encodedBlock = (DataBlock)dataType.createDataBlock(dataBlock.getSize(), dataBlock.getGridPosition(), dataBlock.getNumElements()); + transpose.encode(dataBlock.getData(), encodedBlock.getData(), dataBlock.getSize(), order); + return encodedBlock; + } + + @SuppressWarnings("unchecked") + @Override + public DataBlock decode(DataBlock dataBlock) { + + DataBlock decodedBlock = (DataBlock)dataType.createDataBlock(dataBlock.getSize(), dataBlock.getGridPosition(), dataBlock.getNumElements()); + transpose.decode((T)dataBlock.getData(), decodedBlock.getData(), dataBlock.getSize(), order); + return decodedBlock; + } + + public static boolean isIdentity(int[] permutation) { + + for (int i = 0; i < permutation.length; i++) + if (permutation[i] != i) + return false; + + return true; + } + + public static boolean isReversal(int[] permutation) { + + for (int i = 0; i < permutation.length; i++) + if (permutation[i] != i) + return false; + + return true; + } + + public static int[] invertPermutation(final int[] p) { + + final int[] inv = new int[p.length]; + for (int i = 0; i < p.length; i++) + inv[p[i]] = i; + + return inv; + } + + /** + * Composes two permutations: result[i] = first[second[i]]. + * + * @param first + * the first permutation + * @param second + * the second permutation + * @return the composition of first and second + */ + public static int[] concatenatePermutations(final int[] first, final int[] second) { + + int n = first.length; + final int[] result = new int[n]; + for (int i = 0; i < n; i++) { + result[i] = first[second[i]]; + } + return result; + } + + /** + * Conjugates a permutation with the reversal permutation: rev * p * rev^-1, + * where rev is the permutation that reverses the elements. + * + * @param p + * the permutation to conjugate + * @return the conjugated permutation + */ + public static int[] conjugateWithReverse(final int[] p) { + + final int n = p.length; + final int[] rev = new int[n]; + for (int i = 0; i < n; i++) + rev[i] = n - i - 1; + + // note that rev is its own inverse + int[] result = concatenatePermutations(rev, p); // result = rev * p + result = concatenatePermutations(result, rev); // result = rev * p * + // rev^-1 + return result; + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodecInfo.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodecInfo.java new file mode 100644 index 000000000..550432cef --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodecInfo.java @@ -0,0 +1,96 @@ +package org.janelia.saalfeldlab.n5.codec.transpose; + +import java.util.Arrays; +import java.util.stream.IntStream; + +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.codec.DatasetCodec; +import org.janelia.saalfeldlab.n5.codec.DatasetCodecInfo; +import org.janelia.saalfeldlab.n5.serialization.NameConfig; + +/** + * Describes a permutation of the dimensions of a block. + *

+ * The {@code order} parameter parameterizes the permutation. + * The ith element of the order array gives the destination index of the ith element of the input. + * Example: + * order = [1, 2, 0] + * input = [7, 8, 9] // interpret as a block size + * result = [9, 7, 8] // permuted block size + * + *

+ * See the specification of Zarr's Transpose codec. + */ +@NameConfig.Name(value = TransposeCodecInfo.TYPE) +public class TransposeCodecInfo implements DatasetCodecInfo { + + public static final String TYPE = "n5-transpose"; + + @NameConfig.Parameter + private int[] order; + + public TransposeCodecInfo() { + // for serialization + } + + public TransposeCodecInfo(int[] order) { + + this.order = order; + } + + @Override + public String getType() { + + return TYPE; + } + + public int[] getOrder() { + + return order; + } + + @Override + public DatasetCodec create(DatasetAttributes datasetAttributes) { + + validate(); + return new TransposeCodec(datasetAttributes.getDataType(), getOrder()); + } + + @Override + public boolean equals(Object obj) { + + if (obj instanceof TransposeCodecInfo) + return Arrays.equals(order, ((TransposeCodecInfo)obj).getOrder()); + + return false; + } + + private void validate() { + + final boolean[] indexFound = new boolean[order.length]; + for( int i : order ) + indexFound[i] = true; + + final int[] missingIndexes = IntStream.range(0, order.length).filter(i -> !indexFound[i]).toArray(); + if( missingIndexes.length > 0 ) + throw new N5Exception("Invalid order for TransposeCodec. Missing indexes: " + Arrays.toString(missingIndexes)); + + } + + public static TransposeCodecInfo concatenate(TransposeCodecInfo[] infos) { + + if( infos == null || infos.length == 0) + return null; + + // copy the initial order so we don't modify to the original + int[] order = new int[infos[0].order.length]; + System.arraycopy(infos[0].order, 0, order, 0, order.length); + + for( int i = 1; i < infos.length; i++ ) + order = TransposeCodec.concatenatePermutations(order, infos[i].order); + + return new TransposeCodecInfo(order); + } + +} \ No newline at end of file diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposePlayground.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposePlayground.java new file mode 100644 index 000000000..5b9b29ee7 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposePlayground.java @@ -0,0 +1,84 @@ +package org.janelia.saalfeldlab.n5.codec.transpose; + +import java.util.Arrays; + +import org.janelia.saalfeldlab.n5.DataType; + +/** + * Exploration for implementing "transpose" codec in N5 + */ +public class TransposePlayground { + + public static void main(String[] args) { +// transpose2D(); +// System.out.println("\n\n --------------------------------------- \n\n"); +// transpose3D(); + } + + public static void transpose2D() { + + Transpose transpose = Transpose.of(DataType.UINT32, 2); + + int[] values = { + 1, 2, 3, + 4, 5, 6 + }; + + int[] decoded_size = {3, 2}; + int[] order = {0, 1}; +// int[] order = {1, 0}; + System.out.println("decoded_size = " + Arrays.toString(decoded_size)); + System.out.println("order = " + Arrays.toString(order)); + + System.out.println("values = \n" + toString(values, decoded_size)); + + int[] encoded = new int[values.length]; + transpose.encode(values, encoded, decoded_size, order); + + int[] encoded_size = Transpose.encode(decoded_size, order); + System.out.println("encoded = \n" + toString(encoded, encoded_size)); + + int[] decoded = new int[values.length]; + transpose.decode(encoded, decoded, decoded_size, order); + + System.out.println("decoded = \n" + toString(decoded, decoded_size)); + } + + public static void transpose3D() { + +// Transpose transpose = new Transpose<>(Transpose.MemCopy.INT, 3); + Transpose transpose = Transpose.of(DataType.UINT32, 3); + + int[] decoded_size = {4, 3, 2}; + int[] order = {1, 2, 0}; + System.out.println("decoded_size = " + Arrays.toString(decoded_size)); + System.out.println("order = " + Arrays.toString(order)); + + int[] encoded_size = Transpose.encode(decoded_size, order); + System.out.println("encoded_size = " + Arrays.toString(encoded_size)); + + int[] values = new int[4 * 3 * 2]; + Arrays.setAll(values, i -> i); + System.out.println("values = \n" + toString(values, decoded_size)); + + int[] encoded = new int[values.length]; + transpose.encode(values, encoded, decoded_size, order); + System.out.println("encoded = \n" + toString(encoded, encoded_size)); + + int[] decoded = new int[values.length]; + transpose.decode(encoded, decoded, decoded_size, order); + System.out.println("decoded = \n" + toString(decoded, decoded_size)); + } + + static String toString(int[] values, int[] size) { + StringBuilder str = new StringBuilder(); + final int w = size[0]; + final int h = size[1]; + for (int i = 0; i < values.length; i += w) { + str.append(Arrays.toString(Arrays.copyOfRange(values, i, Math.min(values.length, i + w)))).append("\n"); + if ((i + w) % (w * h) == 0) + str.append("\n"); + } + return str.toString(); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultDatasetAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultDatasetAccess.java index 74f64d6e6..574acbabd 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultDatasetAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultDatasetAccess.java @@ -42,6 +42,7 @@ import org.janelia.saalfeldlab.n5.N5Exception.N5NoSuchKeyException; import org.janelia.saalfeldlab.n5.N5Writer.DataBlockSupplier; import org.janelia.saalfeldlab.n5.codec.BlockCodec; +import org.janelia.saalfeldlab.n5.codec.DatasetCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedPosition; @@ -52,10 +53,16 @@ public class DefaultDatasetAccess implements DatasetAccess { private final NestedGrid grid; private final BlockCodec[] codecs; + private final DatasetCodec[] datasetCodecs; - public DefaultDatasetAccess(final NestedGrid grid, final BlockCodec[] codecs) { + public DefaultDatasetAccess(final NestedGrid grid, final BlockCodec[] codecs, DatasetCodec[] datasetCodecs ) { this.grid = grid; this.codecs = codecs; + this.datasetCodecs = datasetCodecs; + } + + public DefaultDatasetAccess(final NestedGrid grid, final BlockCodec[] codecs) { + this( grid, codecs, new DatasetCodec[0]); } public NestedGrid getGrid() { @@ -65,7 +72,7 @@ public NestedGrid getGrid() { @Override public DataBlock readBlock(final PositionValueAccess pva, final long[] gridPosition) throws N5IOException { final NestedPosition position = grid.nestedPosition(gridPosition); - return readBlockRecursive(pva.get(position.key()), position, grid.numLevels() - 1); + return (DataBlock)decodeWithDatasetCodecs(readBlockRecursive(pva.get(position.key()), position, grid.numLevels() - 1)); } private DataBlock readBlockRecursive( @@ -145,6 +152,7 @@ private List> readShardRecursive( final NestedPosition firstBlock = positions.get(0); final long[] shardPosition = firstBlock.absolute(level); + @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final RawShard shard = codec.decode(readData, shardPosition).getData(); @@ -171,7 +179,10 @@ private List> readShardRecursive( } @Override - public void writeBlock(final PositionValueAccess pva, final DataBlock dataBlock) throws N5IOException { + public void writeBlock(final PositionValueAccess pva, final DataBlock dataBlockArg) throws N5IOException { + + @SuppressWarnings("unchecked") + final DataBlock dataBlock = (DataBlock)encodeWithDatasetCodecs(dataBlockArg); final NestedPosition position = grid.nestedPosition(dataBlock.getGridPosition()); final long[] key = position.key(); @@ -467,4 +478,24 @@ private DataBlock getDataBlock() { return dataBlock; } } + + private DataBlock decodeWithDatasetCodecs(DataBlock block) { + + DataBlock result = block; + for (DatasetCodec codec : datasetCodecs) { + result = codec.decode(result); + } + return result; + } + + private DataBlock encodeWithDatasetCodecs(DataBlock block) { + + DataBlock result = block; + for (DatasetCodec codec : datasetCodecs) { + result = codec.encode(result); + } + return result; + } + + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultShardCodecInfo.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultShardCodecInfo.java index 251d5c6c6..da4799835 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultShardCodecInfo.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultShardCodecInfo.java @@ -30,6 +30,7 @@ import java.util.Arrays; import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.codec.BlockCodec; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.CodecInfo; @@ -62,13 +63,13 @@ public String getType() { @NameConfig.Parameter(value = "index_codecs") private CodecInfo[] indexCodecs; - private transient final BlockCodecInfo innerBlockCodecInfo; + private transient BlockCodecInfo innerBlockCodecInfo; - private transient final DataCodecInfo[] innerDataCodecInfos; + private transient DataCodecInfo[] innerDataCodecInfos; - private transient final BlockCodecInfo indexBlockCodecInfo; + private transient BlockCodecInfo indexBlockCodecInfo; - private transient final DataCodecInfo[] indexDataCodecInfos; + private transient DataCodecInfo[] indexDataCodecInfos; DefaultShardCodecInfo() { // for serialization @@ -94,6 +95,36 @@ public DefaultShardCodecInfo( indexCodecs = concatenateCodecs(indexBlockCodecInfo, indexDataCodecInfos); } + private void build() { + + if (innerBlockCodecInfo != null) + return; + + // sets + // innerBlockCodecInfo, innerDataCodecInfos + // indexBlockCodecInfo, indexDataCodecInfos + // from + // codecs and indexCodecs + + if (codecs[0] instanceof BlockCodecInfo) + innerBlockCodecInfo = (BlockCodecInfo)codecs[0]; + else + throw new N5Exception("Codec at index " + 0 + " must be a BlockCodec."); + + innerDataCodecInfos = new DataCodecInfo[codecs.length - 1]; + for (int i = 1; i < codecs.length; i++) + innerDataCodecInfos[i - 1] = (DataCodecInfo)codecs[i]; + + if (indexCodecs[0] instanceof BlockCodecInfo) + indexBlockCodecInfo = (BlockCodecInfo)indexCodecs[0]; + else + throw new N5Exception("Codec at index " + 0 + " must be a BlockCodec."); + + indexDataCodecInfos = new DataCodecInfo[indexCodecs.length - 1]; + for (int i = 1; i < indexCodecs.length; i++) + indexDataCodecInfos[i - 1] = (DataCodecInfo)indexCodecs[i]; + } + @Override public int[] getInnerBlockSize() { return innerBlockSize; @@ -135,6 +166,8 @@ public CodecInfo[] getIndexCodecs() { @Override public RawShardCodec create(final int[] blockSize, final DataCodecInfo... codecs) { + build(); + // Number of elements (DataBlocks, nested shards) in each dimension per shard. final int[] size = new int[blockSize.length]; // blockSize argument is number of pixels in the shard @@ -161,4 +194,5 @@ private static CodecInfo[] concatenateCodecs(BlockCodecInfo blkInfo, DataCodecIn return allCodecs; } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/FinalPosition.java b/src/main/java/org/janelia/saalfeldlab/n5/util/FinalPosition.java new file mode 100644 index 000000000..1b7076d54 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/util/FinalPosition.java @@ -0,0 +1,38 @@ +package org.janelia.saalfeldlab.n5.util; + +/* + * An immutable {@Position}. + */ +public class FinalPosition implements Position { + + public final long[] position; + + public FinalPosition(long[] position) { + this.position = position; + } + + public FinalPosition(Position p) { + this.position = p.get().clone(); + } + + @Override + public long[] get() { + return position; + } + + @Override + public long get(int i) { + return position[i]; + } + + @Override + public String toString() { + return Position.toString(this); + } + + @Override + public boolean equals(Object obj) { + return Position.equals(this, obj); + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/FloatValueParser.java b/src/main/java/org/janelia/saalfeldlab/n5/util/FloatValueParser.java new file mode 100644 index 000000000..5a2860f8b --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/util/FloatValueParser.java @@ -0,0 +1,110 @@ +package org.janelia.saalfeldlab.n5.util; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.janelia.saalfeldlab.n5.N5Exception; + +/** + * Parses {@link Float} and {@link Double} values from JSON hex strings. + *

+ * This does not directly cover the Strings "NaN", "Infinity", and "-Infinity" + * but they are parsable by the parseDouble and parseFloat methods. Rather, this + * class handles converting to and from the hex representations of NaN, + * -Infinity, Infinity, and all other allowable values. + */ +public class FloatValueParser { + + /** + * Parses a hex string to a float value. + * + * @param hexString + * hex string in format "0x" followed by 8 hex digits + * @return the float value + * @throws N5Exception + * if the string format is invalid + */ + public static float parseFloat(String hexString) throws N5Exception { + + validateFloat(hexString); + final int intValue = Integer.parseUnsignedInt(hexString.substring(2), 16); + return Float.intBitsToFloat(intValue); + } + + /** + * Encodes a float value to a hex string. + * + * @param value + * the float to encode + * @return hex string in format "0x" followed by 8 hex digits + */ + public static String encodeFloat(float value) { + + return String.format("0x%08x", Float.floatToIntBits(value)); + } + + private static void validateFloat(String hexString) { + + if (!hexString.startsWith("0x") || hexString.length() != 10) + throw new N5Exception("Could not parse string " + hexString + " as float."); + } + + /** + * Parses a hex string to a double value. + * + * @param hexString + * hex string in format "0x" followed by 16 hex digits + * @return the double value + * @throws N5Exception + * if the string format is invalid + */ + public static double parseDouble(String hexString) throws N5Exception { + + validateDouble(hexString); + final long longValue = Long.parseUnsignedLong(hexString.substring(2), 16); + return Double.longBitsToDouble(longValue); + } + + /** + * Encodes a double value to a hex string. + * + * @param value + * the double to encode + * @return hex string in format "0x" followed by 16 hex digits + */ + public static String encodeDouble(double value) { + + return String.format("0x%016x", Double.doubleToLongBits(value)); + } + + private static void validateDouble(String hexString) { + + if (!hexString.startsWith("0x") || hexString.length() != 18) + throw new N5Exception("Could not parse string " + hexString + " as double."); + } + + /** + * Parses a hex string to a byte array. + * + * @param hexString + * hex string in format "0x" followed by hex digits + * @return the decoded byte array + * @throws N5Exception + * if the string format is invalid or decoding fails + */ + public static byte[] parseBytes(String hexString) throws N5Exception { + + validateBytes(hexString); + try { + return Hex.decodeHex(hexString.substring(2)); + } catch (DecoderException e) { + throw new N5Exception(e); + } + } + + private static void validateBytes(String hexString) { + + if (!hexString.startsWith("0x")) + throw new N5Exception("Could not parse string " + hexString + " to bytes."); + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/GridIterator.java b/src/main/java/org/janelia/saalfeldlab/n5/util/GridIterator.java new file mode 100644 index 000000000..43258efaf --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/util/GridIterator.java @@ -0,0 +1,178 @@ +package org.janelia.saalfeldlab.n5.util; + +import java.util.Iterator; + +/** + * Essentially imglib2's IntervalIterator, but N5 does not depend on imglib2. + */ +public class GridIterator implements Iterator { + + final protected long[] dimensions; + + final protected long[] steps; + + final protected long[] position; + + final protected int[] intPosition; + + final protected long[] min; + + final protected int lastIndex; + + protected int index = -1; + + public GridIterator(final long[] dimensions, final long[] min) { + + final int n = dimensions.length; + this.dimensions = new long[n]; + this.position = new long[n]; + this.intPosition = new int[n]; + this.min = min; + steps = new long[n]; + + final int m = n - 1; + long k = steps[0] = 1; + for (int d = 0; d < m; ) { + final long dimd = dimensions[d]; + this.dimensions[d] = dimd; + k *= dimd; + steps[++d] = k; + } + final long dimm = dimensions[m]; + this.dimensions[m] = dimm; + lastIndex = (int)(k * dimm - 1); + } + + public GridIterator(final long[] dimensions) { + + this(dimensions, new long[dimensions.length]); + } + + public GridIterator(final int[] dimensions) { + + this(int2long(dimensions)); + } + + public void fwd() { + + ++index; + } + + public void reset() { + + index = -1; + } + + @Override + public boolean hasNext() { + + return index < lastIndex; + } + + @Override + public long[] next() { + + fwd(); + indexToPosition(index, dimensions, min, position); + return position; + } + + public int[] nextInt() { + + next(); + long2int(position, intPosition); + return intPosition; + } + + public int getIndex() { + + return index; + } + + public static void indexToPosition(long index, final long[] dimensions, final long[] offset, + final long[] position) { + + for (int dim = 0; dim < dimensions.length; dim++) { + position[dim] = (index % dimensions[dim]) + offset[dim]; + index /= dimensions[dim]; + } + } + + public static void indexToPosition(long index, final int[] dimensions, final long[] offset, + final long[] position) { + + for (int dim = 0; dim < dimensions.length; dim++) { + position[dim] = (index % dimensions[dim]) + offset[dim]; + index /= dimensions[dim]; + } + } + + final static public long positionToIndex(final long[] dimensions, final long[] position) { + long idx = 0; + int cumulativeSize = 1; + for (int i = 0; i < position.length; i++) { + idx += position[i] * cumulativeSize; + cumulativeSize *= dimensions[i]; + } + return idx; + } + + final static public long positionToIndex(final long[] dimensions, final int[] position) { + long idx = 0; + int cumulativeSize = 1; + for (int i = 0; i < position.length; i++) { + idx += position[i] * cumulativeSize; + cumulativeSize *= dimensions[i]; + } + return idx; + } + + final static public long positionToIndex(final int[] dimensions, final long[] position) { + long idx = 0; + int cumulativeSize = 1; + for (int i = 0; i < position.length; i++) { + idx += position[i] * cumulativeSize; + cumulativeSize *= dimensions[i]; + } + return idx; + } + + final static public long positionToIndex(final int[] dimensions, final int[] position) { + long idx = 0; + int cumulativeSize = 1; + for (int i = 0; i < position.length; i++) { + idx += position[i] * cumulativeSize; + cumulativeSize *= dimensions[i]; + } + return idx; + } + + public static int[] long2int(final long[] src, final int[] tgt) { + + for (int d = 0; d < tgt.length; ++d) + tgt[d] = (int)src[d]; + + return tgt; + } + + public static int[] long2int(final long[] a) { + + final int[] i = new int[a.length]; + + for (int d = 0; d < a.length; ++d) + i[d] = (int)a[d]; + + return i; + } + + public static long[] int2long(final int[] i) { + + final long[] l = new long[i.length]; + + for (int d = 0; d < l.length; ++d) + l[d] = i[d]; + + return l; + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/Position.java b/src/main/java/org/janelia/saalfeldlab/n5/util/Position.java new file mode 100644 index 000000000..2403835b9 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/util/Position.java @@ -0,0 +1,66 @@ +package org.janelia.saalfeldlab.n5.util; + +import java.util.Arrays; + +/* + * A wrapper around a primitive long array that is lexicographically {@link Comparable} + * and for which we can test equality. + */ +public interface Position extends Comparable { + + long[] get(); + + long get(int i); + + default int numDimensions() { + return get().length; + } + + @Override + default int compareTo(Position other) { + + // use Arrays.compare when we update to Java 9+ + final int N = numDimensions() > other.numDimensions() ? numDimensions() : other.numDimensions(); + for (int i = 0; i < N; i++) { + final long diff = get(i) - other.get(i); + if (diff != 0) + return (int) diff; + } + return 0; + } + + static boolean equals(final Position a, final Object b) { + + if (a == null && b == null) + return true; + + if (b == null) + return false; + + if (!(b instanceof Position)) + return false; + + final Position other = (Position) b; + if (other.numDimensions() != a.numDimensions()) + return false; + + for (int i = 0; i < a.numDimensions(); i++) + if (other.get(i) != a.get(i)) + return false; + + return true; + } + + static String toString(Position p) { + return "Position: " + Arrays.toString(p.get()); + } + + static Position wrap(final long[] p) { + return new FinalPosition(p); + } + + static Position wrap(final int[] p) { + return new FinalPosition(GridIterator.int2long(p)); + } + +} diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/ChecksumCodecTests.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/ChecksumCodecTests.java new file mode 100644 index 000000000..da2e73a59 --- /dev/null +++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/ChecksumCodecTests.java @@ -0,0 +1,36 @@ +package org.janelia.saalfeldlab.n5.codec; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.codec.checksum.Crc32cChecksumCodec; +import org.janelia.saalfeldlab.n5.readdata.ReadData; +import org.junit.Test; + +public class ChecksumCodecTests { + + @Test + public void testCrc32cChecksumCodec() { + + final ReadData rd = ReadData.from(new byte[] {0,1,2,3,4,5,6,7,8,9}); + final long N = rd.requireLength(); + + final Crc32cChecksumCodec codec = new Crc32cChecksumCodec(); + final ReadData encoded = codec.encode(rd); + + // Crc32 adds 4 bytes to the data + assertEquals(N+codec.numChecksumBytes(), encoded.requireLength()); + + final ReadData decoded = codec.decode(encoded); + assertArrayEquals(rd.allBytes(), decoded.allBytes()); + + // attempting to decode perturbed data throws exception + final byte[] encodedBytes = encoded.allBytes(); + encodedBytes[1]++; + final ReadData perturbed = ReadData.from(encodedBytes); + assertThrows(N5Exception.class, () -> codec.decode(perturbed)); + } + +} diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecTests.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecTests.java new file mode 100644 index 000000000..2a94bd8d2 --- /dev/null +++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecTests.java @@ -0,0 +1,48 @@ +package org.janelia.saalfeldlab.n5.codec; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.janelia.saalfeldlab.n5.codec.transpose.TransposeCodecInfo; +import org.junit.Test; + +public class DatasetCodecTests { + + @Test + public void testTransposeCodecSimplification() throws Exception { + + // 2d + final TransposeCodecInfo id2 = new TransposeCodecInfo(new int[]{0, 1}); + final TransposeCodecInfo rev2 = new TransposeCodecInfo(new int[]{1, 0}); + + assertNull(TransposeCodecInfo.concatenate(null)); + assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id2})); + assertEquals(rev2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2})); + + assertEquals(rev2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2, id2})); + assertEquals(rev2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id2, rev2, id2})); + + assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2, rev2})); + assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2, rev2, rev2, rev2})); + assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id2, rev2, id2, rev2, rev2, rev2})); + + // 3d + final TransposeCodecInfo id3 = new TransposeCodecInfo(new int[]{0, 1, 2}); + final TransposeCodecInfo rev3 = new TransposeCodecInfo(new int[]{2, 1, 0}); + + final TransposeCodecInfo t021 = new TransposeCodecInfo(new int[]{0, 2, 1}); + final TransposeCodecInfo t102 = new TransposeCodecInfo(new int[]{1, 0, 2}); + final TransposeCodecInfo t120 = new TransposeCodecInfo(new int[]{1, 2, 0}); + final TransposeCodecInfo t201 = new TransposeCodecInfo(new int[]{2, 0, 1}); + + assertEquals(id3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id3})); + assertEquals(rev3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev3})); + + assertEquals(rev3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev3, id3})); + assertEquals(rev3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id3, rev3, id3})); + + assertEquals(t102, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev3, t102, t021})); + assertEquals(t201, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{t021, t102})); + assertEquals(t120, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{t102, t021})); + } +} diff --git a/src/test/java/org/janelia/saalfeldlab/n5/demo/BlockIterators.java b/src/test/java/org/janelia/saalfeldlab/n5/demo/BlockIterators.java new file mode 100644 index 000000000..20b69082a --- /dev/null +++ b/src/test/java/org/janelia/saalfeldlab/n5/demo/BlockIterators.java @@ -0,0 +1,93 @@ +package org.janelia.saalfeldlab.n5.demo; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.RawCompression; +import org.janelia.saalfeldlab.n5.codec.CodecInfo; +import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; +import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; +import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodecInfo; +import org.janelia.saalfeldlab.n5.util.GridIterator; + +public class BlockIterators { + + public static void main(String[] args) { + +// blockIterator(); +// shardBlockIterator(); + } + + public static void shardBlockIterator() { + +// final DatasetAttributes attrs = new DatasetAttributes( +// new long[] {12, 8}, // image size +// new int[] {6, 4}, // shard size +// new int[] {2, 2}, // block size +// DataType.UINT8, +// new ShardingCodec( +// new int[] {2, 2}, +// new CodecInfo[] { new N5BlockCodecInfo() }, +// new DeterministicSizeCodecInfo[] { new RawBlockCodecInfo() }, +// IndexLocation.END +// )); +// +// shardPositions(attrs) +// .forEach(x -> System.out.println(Arrays.toString(x))); + } + +// public static void blockIterator() { +// +// final DatasetAttributes attrs = new DatasetAttributes( +// new long[] {12, 8}, +// new int[] {2, 2}, +// DataType.UINT8, +// new RawCompression()); +// +// blockPositions(attrs).forEach(x -> System.out.println(Arrays.toString(x))); +// } +// +// public static long[] blockGridSize(final DatasetAttributes attrs ) { +// // this could be a nice method for DatasetAttributes +// +// return IntStream.range(0, attrs.getNumDimensions()).mapToLong(i -> (long)Math.ceil((double)attrs.getDimensions()[i] / attrs.getBlockSize()[i])).toArray(); +// +// } +// +// public static long[] shardGridSize(final DatasetAttributes attrs ) { +// // this could be a nice method for DatasetAttributes +// +// return IntStream.range(0, attrs.getNumDimensions()).mapToLong(i -> (long)Math.ceil((double)attrs.getDimensions()[i] / attrs.getShardSize()[i])).toArray(); +// +// } +// +// public static Stream blockPositions( DatasetAttributes attrs ) { +// return toStream(new GridIterator(blockGridSize(attrs))); +// } +// +// public static Stream shardPositions( DatasetAttributes attrs ) { +// +// final int[] blocksPerShard = attrs.getBlocksPerShard(); +// return toStream( new GridIterator(shardGridSize(attrs))) +// .flatMap( shardPosition -> { +// +// final int nd = attrs.getNumDimensions(); +// final long[] min = attrs.getBlockPositionFromShardPosition(shardPosition, new int[nd]); +// return toStream(new GridIterator(GridIterator.int2long(blocksPerShard), min)); +// }); +// } +// +// public static Stream toStream( final Iterator it ) { +// return StreamSupport.stream( Spliterators.spliteratorUnknownSize( +// it, Spliterator.ORDERED), +// false); +// } + +} diff --git a/src/test/java/org/janelia/saalfeldlab/n5/http/HttpReaderFsWriter.java b/src/test/java/org/janelia/saalfeldlab/n5/http/HttpReaderFsWriter.java index aba51a4d0..28139f4da 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/http/HttpReaderFsWriter.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/http/HttpReaderFsWriter.java @@ -33,7 +33,6 @@ import org.janelia.saalfeldlab.n5.CachedGsonKeyValueN5Reader; import org.janelia.saalfeldlab.n5.CachedGsonKeyValueN5Writer; import org.janelia.saalfeldlab.n5.DataBlock; -import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.GsonKeyValueN5Reader; import org.janelia.saalfeldlab.n5.GsonKeyValueN5Writer; @@ -51,8 +50,6 @@ import java.util.concurrent.ExecutorService; import java.util.function.Predicate; -import static org.janelia.saalfeldlab.n5.N5KeyValueReader.ATTRIBUTES_JSON; - public class HttpReaderFsWriter implements GsonKeyValueN5Writer { private final GsonKeyValueN5Writer writer; @@ -263,9 +260,10 @@ public HttpRead return writer.remove(); } - @Override public void createDataset(String datasetPath, DatasetAttributes datasetAttributes) throws N5Exception { + @Override public DatasetAttributes createDataset(String datasetPath, DatasetAttributes datasetAttributes) throws N5Exception { writer.createDataset(datasetPath, datasetAttributes); + return datasetAttributes; } @Override public void writeBlock(String datasetPath, DatasetAttributes datasetAttributes, DataBlock dataBlock) throws N5Exception { diff --git a/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardIndexTest.java b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardIndexTest.java new file mode 100644 index 000000000..ea1406d83 --- /dev/null +++ b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardIndexTest.java @@ -0,0 +1,105 @@ +package org.janelia.saalfeldlab.n5.shard; + +import org.janelia.saalfeldlab.n5.KeyValueAccess; +import org.janelia.saalfeldlab.n5.LockedChannel; +import org.janelia.saalfeldlab.n5.N5FSTest; +import org.janelia.saalfeldlab.n5.N5KeyValueWriter; +import org.janelia.saalfeldlab.n5.codec.IndexCodecAdapter; +import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; +import org.janelia.saalfeldlab.n5.codec.checksum.Crc32cChecksumCodec; +import org.janelia.saalfeldlab.n5.util.GridIterator; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Paths; + +import static org.junit.Assert.assertEquals; + +public class ShardIndexTest { + + private static final N5FSTest tempN5Factory = new N5FSTest(); + + @After + public void removeTempWriters() { + + tempN5Factory.removeTempWriters(); + } + + @Test + @Ignore + public void testOffsetIndex() { + + // TODO +// int[] shardBlockGridSize = new int[]{5, 4, 3}; +// ShardIndex index = new ShardIndex( +// shardBlockGridSize, +// IndexLocation.END, new RawBlockCodecInfo()); +// +// GridIterator it = new GridIterator(shardBlockGridSize); +// int i = 0; +// while (it.hasNext()) { +// int j = index.getOffsetIndex(GridIterator.long2int(it.next())); +// assertEquals(i, j); +// i += 2; +// } +// +// shardBlockGridSize = new int[]{5, 4, 3, 13}; +// index = new ShardIndex( +// shardBlockGridSize, +// IndexLocation.END, new RawBlockCodecInfo()); +// +// it = new GridIterator(shardBlockGridSize); +// i = 0; +// while (it.hasNext()) { +// int j = index.getOffsetIndex(GridIterator.long2int(it.next())); +// assertEquals(i, j); +// i += 2; +// } + + } + + @Test + @Ignore + public void writeReadTest() throws IOException { + + // TODO + +// final N5KeyValueWriter writer = (N5KeyValueWriter)tempN5Factory.createTempN5Writer(); +// final KeyValueAccess kva = writer.getKeyValueAccess(); +// +// final int[] shardBlockGridSize = new int[]{6, 5}; +// final IndexLocation indexLocation = IndexLocation.END; +// final IndexCodecAdapter indexCodecAdapter = new IndexCodecAdapter( +// new RawBlockCodecInfo(), +// new Crc32cChecksumCodec() +// ); +// +// final ShardIndex index = new ShardIndex(shardBlockGridSize, indexLocation, indexCodecAdapter); +// index.set(0, 6, new int[]{0, 0}); +// index.set(19, 32, new int[]{1, 0}); +// index.set(93, 111, new int[]{3, 0}); +// index.set(143, 1, new int[]{1, 2}); +// +// final String path = Paths.get(Paths.get(writer.getURI()).toAbsolutePath().toString(), "indexTest").toString(); +// try ( +// final LockedChannel channel = kva.lockForWriting(path); +// final OutputStream out = channel.newOutputStream() +// ) { +// +// ShardIndex.write(out, index); +// } +// +// final ShardIndex indexRead = new ShardIndex(shardBlockGridSize, indexLocation, indexCodecAdapter); +// try ( +// final LockedChannel channel = kva.lockForReading(path); +// final InputStream in = channel.newInputStream() +// ) { +// ShardIndex.read(in, indexRead); +// } +// assertEquals(index, indexRead); + } +}