From b6c5c171c2a32cd369e139c2c056b485db908e93 Mon Sep 17 00:00:00 2001 From: yash-puligundla Date: Wed, 7 Feb 2024 13:55:16 -0500 Subject: [PATCH] Add FQZComp Decoder --- .../compression/fqzcomp/FQZCompDecode.java | 301 ++++++++++++++++++ .../compression/fqzcomp/FQZGlobalFlags.java | 28 ++ .../cram/compression/fqzcomp/FQZModel.java | 56 ++++ .../cram/compression/fqzcomp/FQZParam.java | 205 ++++++++++++ .../cram/compression/fqzcomp/FQZState.java | 88 +++++ .../cram/compression/range/RangeCoder.java | 4 +- .../samtools/cram/FQZCompInteropTest.java | 80 +++++ 7 files changed, 760 insertions(+), 2 deletions(-) create mode 100644 src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZCompDecode.java create mode 100644 src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZGlobalFlags.java create mode 100644 src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZModel.java create mode 100644 src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZParam.java create mode 100644 src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZState.java create mode 100644 src/test/java/htsjdk/samtools/cram/FQZCompInteropTest.java diff --git a/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZCompDecode.java b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZCompDecode.java new file mode 100644 index 0000000000..5933a7bcaf --- /dev/null +++ b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZCompDecode.java @@ -0,0 +1,301 @@ +package htsjdk.samtools.cram.compression.fqzcomp; + +import htsjdk.samtools.cram.CRAMException; +import htsjdk.samtools.cram.compression.CompressionUtils; +import htsjdk.samtools.cram.compression.range.ByteModel; +import htsjdk.samtools.cram.compression.range.RangeCoder; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class FQZCompDecode { + private static final int NUMBER_OF_SYMBOLS = 256; + + public static ByteBuffer uncompress( final ByteBuffer inBuffer) { + final int bufferLength = CompressionUtils.readUint7(inBuffer); + final int version = inBuffer.get() & 0xFF; + if (version != 5) { + throw new CRAMException("Invalid FQZComp format version number: " + version); + } + final FQZGlobalFlags globalFlags = new FQZGlobalFlags(inBuffer.get() & 0xFF); + final int numParamBlock = globalFlags.isMultiParam()?inBuffer.get() : 1; + int maxSelector = (numParamBlock > 1) ? (numParamBlock - 1) : 0; + final int[] selectorTable = new int[NUMBER_OF_SYMBOLS]; + if (globalFlags.hasSelectorTable()) { + maxSelector = inBuffer.get() & 0xFF; + readArray(inBuffer, selectorTable, NUMBER_OF_SYMBOLS); + } else { + for (int i = 0; i < numParamBlock; i++) { + selectorTable[i] = i; + } + for (int i = numParamBlock; i < NUMBER_OF_SYMBOLS; i++) { + selectorTable[i] = numParamBlock - 1; + } + } + final List fqzParamList = new ArrayList(numParamBlock); + int maxSymbols = 0; // maximum number of distinct Quality values across all param sets + for (int p=0; p < numParamBlock; p++){ + fqzParamList.add(p,decodeFQZSingleParam(inBuffer)); + if(maxSymbols < fqzParamList.get(p).getMaxSymbols()){ + maxSymbols = fqzParamList.get(p).getMaxSymbols(); + } + } + + // main decode loop + int i = 0; + final FQZState fqzState = new FQZState(); + final RangeCoder rangeCoder = new RangeCoder(); + rangeCoder.rangeDecodeStart(inBuffer); + final FQZModel model = fqzCreateModels(maxSymbols, maxSelector); + final List QualityLengths = new ArrayList<>(); + FQZParam params = null; + int last = 0; + final int[] rev = null; + final ByteBuffer outBuffer = CompressionUtils.allocateByteBuffer(bufferLength); + while (i 0) + rle[j++] = run; + } + last = run; + } + + // Now expand runs in rle to table, noting 255 is max run + int i = 0; + j = 0; + z = 0; + int part; + while (z < size) { + int run_len = 0; + do { + part = rle[j++]; + run_len += part; + } while (part == 255); + + while (run_len-- > 0) + table[z++] = i; + i++; + } + } + + public static FQZModel fqzCreateModels(final int maxSymbols, final int maxSelector){ + final FQZModel fqzModel = new FQZModel(); + fqzModel.setQuality(new ByteModel[1 << 16]); + for (int i = 0; i < (1 << 16); i++) { + fqzModel.getQuality()[i] = new ByteModel(maxSymbols + 1); // +1 as max value not num. values + } + fqzModel.setLength(new ByteModel[4]); + for (int i = 0; i < 4; i++) { + fqzModel.getLength()[i] = new ByteModel(NUMBER_OF_SYMBOLS); + } + fqzModel.setReverse(new ByteModel(2)); + fqzModel.setDuplicate(new ByteModel(2)); + if (maxSelector > 0) { + fqzModel.setSelector(new ByteModel(maxSelector + 1)); + } + return fqzModel; + } + + // If duplicate returns 1, else 0 + public static void decodeFQZNewRecord( + final ByteBuffer inBuffer, + final RangeCoder rangeCoder, + final FQZModel model, + final FQZState state, + final int maxSelector, + final boolean doReverse, + final int[] selectorTable, + final List fqzParamList, + final int[] rev){ + + // Parameter selector + if (maxSelector > 0) { + state.setSelector(model.getSelector().modelDecode(inBuffer, rangeCoder)); + } else { + state.setSelector(0); + } + state.setSelectorTable(selectorTable[state.getSelector()]); + final FQZParam params = fqzParamList.get(state.getSelectorTable()); + + // Reset contexts at the start of each new record + int len; + if (params.getFixedLen() >= 0) { + // Not fixed or fixed but first record + len = model.getLength()[0].modelDecode(inBuffer, rangeCoder); + len |= model.getLength()[1].modelDecode(inBuffer, rangeCoder) << 8; + len |= model.getLength()[2].modelDecode(inBuffer, rangeCoder) << 16; + len |= model.getLength()[3].modelDecode(inBuffer, rangeCoder) << 24; + if (params.getFixedLen() > 0) { + params.setFixedLen(-len); + } + } else { + len = -params.getFixedLen(); + } + state.setRecordLength(len); + if (doReverse) { + rev[state.getRecordNumber()] = model.getReverse().modelDecode(inBuffer, rangeCoder); + } + state.setIsDuplicate(false); + if (params.isDoDedup()) { + if (model.getDuplicate().modelDecode(inBuffer, rangeCoder) != 0) { + state.setIsDuplicate(true); + } + } + state.setBases(len); // number of remaining bytes in this record + state.setDelta(0); + state.setQualityContext(0); + state.setPreviousQuality(0); + state.setRecordNumber(state.getRecordNumber() + 1); + } + + public static int fqzUpdateContext(final FQZParam params, + final FQZState state, + final int quality){ + + int last = params.getContext(); + state.setQualityContext(((state.getQualityContext() << params.getQualityContextShift()) + params.getQualityContextTable()[quality]) >>> 0); + last += ((state.getQualityContext() & ((1 << params.getQualityContextBits()) - 1)) << params.getQualityContextLocation()) >>> 0; + + if (params.isDoPos()) + last += params.getPositionContextTable()[Math.min(state.getBases(), 1023)] << params.getPositionContextLocation(); + + if (params.isDoDelta()) { + last += params.getDeltaContextTable()[Math.min(state.getDelta(), 255)] << params.getDeltaContextLocation(); + state.setDelta(state.getDelta()+ ((state.getPreviousQuality() != quality) ? 1 : 0)); + state.setPreviousQuality(quality); + } + if (params.isDoSel()) + last += state.getSelector() << params.getSelectorContextLocation(); + state.setBases(state.getBases()-1); + return last & 0xffff; + } + + public static FQZParam decodeFQZSingleParam(ByteBuffer inBuffer) { + final FQZParam param = new FQZParam(); + param.setContext((inBuffer.get() & 0xFF) | ((inBuffer.get() & 0xFF) << 8)); + param.setParameterFlags(inBuffer.get() & 0xFF); + param.setMaxSymbols(inBuffer.get() & 0xFF); + final int x = inBuffer.get() & 0xFF; + param.setQualityContextBits(x >> 4); + param.setQualityContextShift(x & 0x0F); + final int y = inBuffer.get() & 0xFF; + param.setQualityContextLocation(y >> 4); + param.setSelectorContextLocation(y & 0x0F); + final int z = inBuffer.get() & 0xFF; + param.setPositionContextLocation(z >> 4); + param.setDeltaContextLocation(z & 0x0F); + + // Read Quality Map. Example: "unbin" Illumina Qualities + param.setQualityMap(new int[NUMBER_OF_SYMBOLS]); + if (param.isDoQmap()) { + for (int i = 0; i < param.getMaxSymbols(); i++) { + param.getQualityMap()[i] = inBuffer.get() & 0xFF; + } + } else { + for (int i = 0; i < NUMBER_OF_SYMBOLS; i++) { + param.getQualityMap()[i] = i; + } + } + + // Read tables + param.setQualityContextTable(new int[1024]); + if (param.getQualityContextBits() > 0 && param.isDoQtab()) { + readArray(inBuffer, param.getQualityContextTable(), NUMBER_OF_SYMBOLS); + } else { + for (int i = 0; i < NUMBER_OF_SYMBOLS; i++) { + param.getQualityContextTable()[i] = i; // NOP + } + } + param.setPositionContextTable(new int[1024]); + if (param.isDoPos()) { + readArray(inBuffer, param.getPositionContextTable(), 1024); + } + param.setDeltaContextTable(new int[NUMBER_OF_SYMBOLS]); + if (param.isDoDelta()) { + readArray(inBuffer, param.getDeltaContextTable(), NUMBER_OF_SYMBOLS); + } + return param; + } + + public static void reverseQualities( + final ByteBuffer outBuffer, + final int bufferLength, + final int[] rev, + final List QualityLengths + ){ + int rec = 0; + int idx = 0; + while (idx< bufferLength) { + if (rev[rec]==1) { + int j = 0; + int k = QualityLengths.get(rec) - 1; + while (j < k) { + byte tmp = outBuffer.get(idx + j); + outBuffer.put(idx + j,outBuffer.get(idx + k)); + outBuffer.put(idx + k, tmp); + j++; + k--; + } + } + idx += QualityLengths.get(rec++); + } + } + +} \ No newline at end of file diff --git a/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZGlobalFlags.java b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZGlobalFlags.java new file mode 100644 index 0000000000..937b7eed62 --- /dev/null +++ b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZGlobalFlags.java @@ -0,0 +1,28 @@ +package htsjdk.samtools.cram.compression.fqzcomp; + +public class FQZGlobalFlags { + public static final int MULTI_PARAM_FLAG_MASK = 0x01; + public static final int SELECTOR_TABLE_FLAG_MASK = 0x02; + public static final int DO_REVERSE_FLAG_MASK = 0x04; + + private int globalFlags; + + public FQZGlobalFlags(final int globalFlags) { + this.globalFlags = globalFlags; + } + + // returns True if more than one parameter block is present + public boolean isMultiParam(){ + return ((globalFlags & MULTI_PARAM_FLAG_MASK)!=0); + } + + // returns True if the parameter selector is mapped through selector table + public boolean hasSelectorTable(){ + return ((globalFlags & SELECTOR_TABLE_FLAG_MASK)!=0); + } + + public boolean doReverse(){ + return ((globalFlags & DO_REVERSE_FLAG_MASK)!=0); + } + +} \ No newline at end of file diff --git a/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZModel.java b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZModel.java new file mode 100644 index 0000000000..047c387a2a --- /dev/null +++ b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZModel.java @@ -0,0 +1,56 @@ +package htsjdk.samtools.cram.compression.fqzcomp; + +import htsjdk.samtools.cram.compression.range.ByteModel; + +public class FQZModel { + + private ByteModel[] quality; // Primary model for quality values + private ByteModel[] length; // Read length models with the context 0-3 being successive byte numbers (little endian order) + private ByteModel reverse; // indicates which strings to reverse + private ByteModel duplicate; // Indicates if this whole string is a duplicate of the last one + private ByteModel selector; // Used if gflags.multi_param or pflags.do_sel are defined. + + public FQZModel() { + } + + public ByteModel[] getQuality() { + + return quality; + } + + public void setQuality(ByteModel[] quality) { + this.quality = quality; + } + + public ByteModel[] getLength() { + return length; + } + + public void setLength(ByteModel[] length) { + this.length = length; + } + + public ByteModel getReverse() { + return reverse; + } + + public void setReverse(ByteModel reverse) { + this.reverse = reverse; + } + + public ByteModel getDuplicate() { + return duplicate; + } + + public void setDuplicate(ByteModel duplicate) { + this.duplicate = duplicate; + } + + public ByteModel getSelector() { + return selector; + } + + public void setSelector(ByteModel selector) { + this.selector = selector; + } +} \ No newline at end of file diff --git a/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZParam.java b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZParam.java new file mode 100644 index 0000000000..eaf9b9d08c --- /dev/null +++ b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZParam.java @@ -0,0 +1,205 @@ +package htsjdk.samtools.cram.compression.fqzcomp; + +public class FQZParam { + private int context; + private int parameterFlags; // Per-parameter block bit-flags + // TODO: rename - follow names from spec. These flags should be set using parameterFlags value + private boolean doDedup; + private int fixedLen; + private boolean doSel; + private boolean doQmap; + private boolean doPos; + private boolean doDelta; + private boolean doQtab; + + private int maxSymbols; // Total number of distinct quality values + private int qualityContextBits; // Total number of bits for Quality context + private int qualityContextShift; // Left bit shift per successive quality in quality context + private int qualityContextLocation; // Bit position of quality context + private int selectorContextLocation; // Bit position of selector context + private int positionContextLocation; // Bit position of position context + private int deltaContextLocation; // Bit position of delta context + private int[] qualityMap; // Map for unbinning quality values. + private int[] qualityContextTable; // Quality context lookup table + private int[] positionContextTable; // Position context lookup table + private int[] deltaContextTable; // Delta context lookup table + + private static final int DEDUP_FLAG_MASK = 0x02; + private static final int FIXED_LEN_FLAG_MASK = 0x04; + private static final int SEL_FLAG_MASK = 0x08; + private static final int QMAP_FLAG_MASK = 0x10; + private static final int PTAB_FLAG_MASK = 0x20; + private static final int DTAB_FLAG_MASK = 0x40; + private static final int QTAB_FLAG_MASK = 0x80; + + public FQZParam() { + } + + public int getContext() { + return context; + } + + public int getParameterFlags() { + return parameterFlags; + } + + public boolean isDoDedup() { + return doDedup; + } + + public int getFixedLen() { + return fixedLen; + } + + public boolean isDoSel() { + return doSel; + } + + public boolean isDoQmap() { + return doQmap; + } + + public boolean isDoPos() { + return doPos; + } + + public boolean isDoDelta() { + return doDelta; + } + + public boolean isDoQtab() { + return doQtab; + } + + public int getMaxSymbols() { + return maxSymbols; + } + + public int getQualityContextBits() { + return qualityContextBits; + } + + public int getQualityContextShift() { + return qualityContextShift; + } + + public int getQualityContextLocation() { + return qualityContextLocation; + } + + public int getSelectorContextLocation() { + return selectorContextLocation; + } + + public int getPositionContextLocation() { + return positionContextLocation; + } + + public int getDeltaContextLocation() { + return deltaContextLocation; + } + + public int[] getQualityMap() { + return qualityMap; + } + + public int[] getQualityContextTable() { + return qualityContextTable; + } + + public int[] getPositionContextTable() { + return positionContextTable; + } + + public int[] getDeltaContextTable() { + return deltaContextTable; + } + + public void setContext(int context) { + this.context = context; + } + + public void setParameterFlags(int parameterFlags) { + this.parameterFlags = parameterFlags; + setDoDedup((parameterFlags & DEDUP_FLAG_MASK) != 0); + setFixedLen(parameterFlags & FIXED_LEN_FLAG_MASK); + setDoSel((parameterFlags & SEL_FLAG_MASK) != 0); + setDoQmap((parameterFlags & QMAP_FLAG_MASK) != 0); + setDoPos((parameterFlags & PTAB_FLAG_MASK) != 0); + setDoDelta((parameterFlags & DTAB_FLAG_MASK) != 0); + setDoQtab((parameterFlags & QTAB_FLAG_MASK) != 0); + } + + public void setDoDedup(boolean doDedup) { + this.doDedup = doDedup; + } + + public void setFixedLen(int fixedLen) { + this.fixedLen = fixedLen; + } + + public void setDoSel(boolean doSel) { + this.doSel = doSel; + } + + public void setDoQmap(boolean doQmap) { + this.doQmap = doQmap; + } + + public void setDoPos(boolean doPos) { + this.doPos = doPos; + } + + public void setDoDelta(boolean doDelta) { + this.doDelta = doDelta; + } + + public void setDoQtab(boolean doQtab) { + this.doQtab = doQtab; + } + + public void setMaxSymbols(int maxSymbols) { + this.maxSymbols = maxSymbols; + } + + public void setQualityContextBits(int qualityContextBits) { + this.qualityContextBits = qualityContextBits; + } + + public void setQualityContextShift(int qualityContextShift) { + this.qualityContextShift = qualityContextShift; + } + + public void setQualityContextLocation(int qualityContextLocation) { + this.qualityContextLocation = qualityContextLocation; + } + + public void setSelectorContextLocation(int selectorContextLocation) { + this.selectorContextLocation = selectorContextLocation; + } + + public void setPositionContextLocation(int positionContextLocation) { + this.positionContextLocation = positionContextLocation; + } + + public void setDeltaContextLocation(int deltaContextLocation) { + this.deltaContextLocation = deltaContextLocation; + } + + public void setQualityMap(int[] qualityMap) { + this.qualityMap = qualityMap; + } + + public void setQualityContextTable(int[] qualityContextTable) { + this.qualityContextTable = qualityContextTable; + } + + public void setPositionContextTable(int[] positionContextTable) { + this.positionContextTable = positionContextTable; + } + + public void setDeltaContextTable(int[] deltaContextTable) { + this.deltaContextTable = deltaContextTable; + } + +} \ No newline at end of file diff --git a/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZState.java b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZState.java new file mode 100644 index 0000000000..3a4981029c --- /dev/null +++ b/src/main/java/htsjdk/samtools/cram/compression/fqzcomp/FQZState.java @@ -0,0 +1,88 @@ +package htsjdk.samtools.cram.compression.fqzcomp; + +public class FQZState { + private int qualityContext; // Qual-only sub-context + private int previousQuality; // Previous quality value + private int delta; // Running delta (quality vs previousQuality) + private int bases; // Number of bases left in current record + private int selector; // Current parameter selector value (0 if unused) + private int selectorTable; // "stab" tabulated copy of s + private int recordLength; // Length of current string + private boolean isDuplicate; // This string is a duplicate of last + private int recordNumber; // Record number + + public FQZState() { + } + + public int getQualityContext() { + return qualityContext; + } + + public void setQualityContext(int qualityContext) { + this.qualityContext = qualityContext; + } + + public int getPreviousQuality() { + return previousQuality; + } + + public void setPreviousQuality(int previousQuality) { + this.previousQuality = previousQuality; + } + + public int getDelta() { + return delta; + } + + public void setDelta(int delta) { + this.delta = delta; + } + + public int getBases() { + return bases; + } + + public void setBases(int bases) { + this.bases = bases; + } + + public int getSelector() { + return selector; + } + + public void setSelector(int selector) { + this.selector = selector; + } + + public int getSelectorTable() { + return selectorTable; + } + + public void setSelectorTable(int selectorTable) { + this.selectorTable = selectorTable; + } + + public int getRecordLength() { + return recordLength; + } + + public void setRecordLength(int recordLength) { + this.recordLength = recordLength; + } + + public boolean getIsDuplicate() { + return isDuplicate; + } + + public void setIsDuplicate(boolean isDuplicate) { + this.isDuplicate = isDuplicate; + } + + public int getRecordNumber() { + return recordNumber; + } + + public void setRecordNumber(int recordNumber) { + this.recordNumber = recordNumber; + } +} \ No newline at end of file diff --git a/src/main/java/htsjdk/samtools/cram/compression/range/RangeCoder.java b/src/main/java/htsjdk/samtools/cram/compression/range/RangeCoder.java index a7d7b21828..f0d7d82911 100644 --- a/src/main/java/htsjdk/samtools/cram/compression/range/RangeCoder.java +++ b/src/main/java/htsjdk/samtools/cram/compression/range/RangeCoder.java @@ -11,7 +11,7 @@ public class RangeCoder { private boolean carry; private int cache; - protected RangeCoder() { + public RangeCoder() { // Spec: RangeEncodeStart this.low = 0; this.range = Constants.MAX_RANGE; // 4 bytes of all 1's @@ -21,7 +21,7 @@ protected RangeCoder() { this.cache = 0; } - protected void rangeDecodeStart(final ByteBuffer inBuffer){ + public void rangeDecodeStart(final ByteBuffer inBuffer){ for (int i = 0; i < 5; i++){ code = (code << 8) + (inBuffer.get() & 0xFF); } diff --git a/src/test/java/htsjdk/samtools/cram/FQZCompInteropTest.java b/src/test/java/htsjdk/samtools/cram/FQZCompInteropTest.java new file mode 100644 index 0000000000..ab9ad4a517 --- /dev/null +++ b/src/test/java/htsjdk/samtools/cram/FQZCompInteropTest.java @@ -0,0 +1,80 @@ +package htsjdk.samtools.cram; + +import htsjdk.HtsjdkTest; +import htsjdk.samtools.cram.compression.CompressionUtils; +import htsjdk.samtools.cram.compression.fqzcomp.FQZCompDecode; +import org.apache.commons.compress.utils.IOUtils; +import org.testng.Assert; +import org.testng.SkipException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class FQZCompInteropTest extends HtsjdkTest { + + public static final String COMPRESSED_FQZCOMP_DIR = "fqzcomp"; + + // uses the available compressed interop test files + @DataProvider(name = "decodeOnlyTestCases") + public Object[][] getDecodeOnlyTestCases() throws IOException { + + // params: + // compressed testfile path, uncompressed testfile path, + // FQZComp decoder + final List testCases = new ArrayList<>(); + for (Path path : CRAMInteropTestUtils.getInteropCompressedFilePaths(COMPRESSED_FQZCOMP_DIR)) { + Object[] objects = new Object[]{ + path, + CRAMInteropTestUtils.getUnCompressedFilePath(path), + new FQZCompDecode() + }; + testCases.add(objects); + } + return testCases.toArray(new Object[][]{}); + } + + @Test(description = "Test if CRAM Interop Test Data is available") + public void testHtsCodecsCorpusIsAvailable() { + if (!CRAMInteropTestUtils.isInteropTestDataAvailable()) { + throw new SkipException(String.format("CRAM Interop Test Data is not available at %s", + CRAMInteropTestUtils.INTEROP_TEST_FILES_PATH)); + } + } + + @Test ( + dependsOnMethods = "testHtsCodecsCorpusIsAvailable", + dataProvider = "decodeOnlyTestCases", + description = "Uncompress the existing compressed file using htsjdk FQZComp and compare it with the original file.") + public void testDecodeOnly( + final Path compressedFilePath, + final Path uncompressedInteropPath, + final FQZCompDecode fqzcompDecode) throws IOException { + try (final InputStream uncompressedInteropStream = Files.newInputStream(uncompressedInteropPath); + final InputStream preCompressedInteropStream = Files.newInputStream(compressedFilePath) + ) { + // preprocess the uncompressed data (to match what the htscodecs-library test harness does) + // by filtering out the embedded newlines, and then round trip through FQZComp codec + // and compare the results + final ByteBuffer uncompressedInteropBytes = CompressionUtils.wrap(CRAMInteropTestUtils.filterEmbeddedNewlines(IOUtils.toByteArray(uncompressedInteropStream))); + final ByteBuffer preCompressedInteropBytes = CompressionUtils.wrap(IOUtils.toByteArray(preCompressedInteropStream)); + + // Use htsjdk to uncompress the precompressed file from htscodecs repo + final ByteBuffer uncompressedHtsjdkBytes = fqzcompDecode.uncompress(preCompressedInteropBytes); + + // Compare the htsjdk uncompressed bytes with the original input file from htscodecs repo + Assert.assertEquals(uncompressedHtsjdkBytes, uncompressedInteropBytes); + } catch (final NoSuchFileException ex){ + throw new SkipException("Skipping testDecodeOnly as either input file " + + "or precompressed file is missing.", ex); + } + } + +} \ No newline at end of file