From 9628c1e6fe0e6299f76c772b265f2d171afc0fbc Mon Sep 17 00:00:00 2001 From: michaelgatzen <52426291+michaelgatzen@users.noreply.github.com> Date: Wed, 21 Aug 2019 17:23:03 -0400 Subject: [PATCH] Added alignment type to RecordAndOffset class (#1411) * Added alignment type to RecordAndOffset class - This was suggested as part of https://github.com/broadinstitute/picard/pull/1370 - necessary to determine whether a RecordAndOffset object is an (alignment) match, insertion, or deletion when queried by a SamLocusIterator - default behavior is preserved by overloading the default constructor * Added tests for including the alignment type in RecordAndOffset --- .../util/AbstractRecordAndOffset.java | 37 ++++++++ .../samtools/util/SamLocusIterator.java | 14 ++- .../samtools/util/SamLocusIteratorTest.java | 88 +++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/main/java/htsjdk/samtools/util/AbstractRecordAndOffset.java b/src/main/java/htsjdk/samtools/util/AbstractRecordAndOffset.java index e76b666839..fd609148a7 100644 --- a/src/main/java/htsjdk/samtools/util/AbstractRecordAndOffset.java +++ b/src/main/java/htsjdk/samtools/util/AbstractRecordAndOffset.java @@ -37,6 +37,16 @@ */ public class AbstractRecordAndOffset { + /** + * Classifies whether the given event is a match, insertion, or deletion. This is deliberately not expressed + * as CIGAR operators, since there is no knowledge of the CIGAR string at the time that this is determined. + */ + public enum AlignmentType { + Match, + Insertion, + Deletion, + } + /** * A SAMRecord aligned to reference position */ @@ -46,6 +56,12 @@ public class AbstractRecordAndOffset { */ protected final int offset; + /** + * The {@link AlignmentType} of this object, which classifies whether the given event is a match, insertion, or + * deletion when queried from a {@link SamLocusIterator}. + */ + protected final AlignmentType alignmentType; + /** * @param record inner SAMRecord * @param offset from the start of the read @@ -53,6 +69,19 @@ public class AbstractRecordAndOffset { public AbstractRecordAndOffset(final SAMRecord record, final int offset) { this.offset = offset; this.record = record; + this.alignmentType = AlignmentType.Match; + } + + /** + * @param record inner SAMRecord + * @param offset from the start of the read + * @param alignmentType The {@link AlignmentType} of this object, which is used when queried in + * a {@link SamLocusIterator}. + */ + public AbstractRecordAndOffset(final SAMRecord record, final int offset, final AlignmentType alignmentType) { + this.offset = offset; + this.record = record; + this.alignmentType = alignmentType; } /** @@ -69,6 +98,14 @@ public SAMRecord getRecord() { return record; } + /** + * The {@link AlignmentType} of this object, which classifies whether the given event is a match, insertion, or + * deletion when queried from a {@link SamLocusIterator}. + */ + public AlignmentType getAlignmentType() { + return alignmentType; + } + /** * @return the read base according to offset. */ diff --git a/src/main/java/htsjdk/samtools/util/SamLocusIterator.java b/src/main/java/htsjdk/samtools/util/SamLocusIterator.java index 028d99dce0..1034f145f3 100644 --- a/src/main/java/htsjdk/samtools/util/SamLocusIterator.java +++ b/src/main/java/htsjdk/samtools/util/SamLocusIterator.java @@ -206,6 +206,16 @@ public static class RecordAndOffset extends AbstractRecordAndOffset { public RecordAndOffset(final SAMRecord record, final int offset) { super(record, offset); } + + /** + * @param record inner SAMRecord + * @param offset 0-based offset from the start of SAMRecord + * @param alignmentType The {@link AlignmentType} of this object, which is used when queried in + * a {@link SamLocusIterator}. + */ + public RecordAndOffset(final SAMRecord record, final int offset, final AlignmentType alignmentType) { + super(record, offset, alignmentType); + } } /** @@ -234,7 +244,7 @@ public void addDeleted(final SAMRecord read, int previousPosition) { if (deletedInRecord == null) { deletedInRecord = new ArrayList<>(); } - deletedInRecord.add(new RecordAndOffset(read, previousPosition)); + deletedInRecord.add(new RecordAndOffset(read, previousPosition, AbstractRecordAndOffset.AlignmentType.Deletion)); } /** @@ -247,7 +257,7 @@ public void addInserted(final SAMRecord read, int firstPosition) { if (insertedInRecord == null) { insertedInRecord = new ArrayList<>(); } - insertedInRecord.add(new RecordAndOffset(read, firstPosition)); + insertedInRecord.add(new RecordAndOffset(read, firstPosition, AbstractRecordAndOffset.AlignmentType.Insertion)); } public List getDeletedInRecord() { diff --git a/src/test/java/htsjdk/samtools/util/SamLocusIteratorTest.java b/src/test/java/htsjdk/samtools/util/SamLocusIteratorTest.java index 64770f7de7..2b8ea58c5f 100644 --- a/src/test/java/htsjdk/samtools/util/SamLocusIteratorTest.java +++ b/src/test/java/htsjdk/samtools/util/SamLocusIteratorTest.java @@ -62,6 +62,10 @@ public void testBasicIterator() { for (final SamLocusIterator.LocusInfo li : sli) { Assert.assertEquals(li.getPosition(), pos++); Assert.assertEquals(li.getRecordAndOffsets().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), coverage); // make sure that we are not accumulating indels Assert.assertEquals(li.getDeletedInRecord().size(), 0); @@ -87,6 +91,10 @@ public void testMissingQualityString() { for (final SamLocusIterator.LocusInfo li : sli) { Assert.assertEquals(li.getPosition(), pos++); Assert.assertEquals(li.getRecordAndOffsets().size(), 2); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), 2); } } @@ -124,6 +132,10 @@ public void testEmitUncoveredLoci() { expectedReads = 0; } Assert.assertEquals(li.getRecordAndOffsets().size(), expectedReads); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), expectedReads); // make sure that we are not accumulating indels Assert.assertEquals(li.getDeletedInRecord().size(), 0); @@ -162,6 +174,10 @@ public void testQualityFilter() { int pos = startPosition; for (final SamLocusIterator.LocusInfo li : sli) { Assert.assertEquals(li.getRecordAndOffsets().size(), (pos % 2 == 0) ? coverage / 2 : coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), (pos % 2 == 0) ? coverage / 2 : coverage); Assert.assertEquals(li.getPosition(), pos++); // make sure that we are not accumulating indels @@ -205,10 +221,18 @@ public void testSimpleDeletion() { // make sure that we are accumulating indels Assert.assertEquals(li.getDeletedInRecord().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getDeletedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Deletion); + } Assert.assertEquals(li.getInsertedInRecord().size(), 0); } else { // make sure we are accumulating normal coverage Assert.assertEquals(li.getRecordAndOffsets().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), coverage); // make sure that we are not accumulating indels @@ -242,12 +266,20 @@ public void testSimpleInsertion() { Assert.assertEquals(li.getPosition(), pos++); // make sure we are accumulating normal coverage Assert.assertEquals(li.getRecordAndOffsets().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), coverage); // make sure that we are not accumulating deletions Assert.assertEquals(li.getDeletedInRecord().size(), 0); if (incIndels && li.getPosition() == insStart) { Assert.assertEquals(li.getInsertedInRecord().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getInsertedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Insertion); + } } else { Assert.assertEquals(li.getInsertedInRecord().size(), 0); } @@ -281,12 +313,20 @@ public void testStartWithInsertion() { Assert.assertEquals(li.getPosition(), pos); // accumulation of coverage Assert.assertEquals(li.getRecordAndOffsets().size(), (indelPosition) ? 0 : coverage + 1); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), (indelPosition) ? 0 : coverage + 1); // no accumulation of deletions Assert.assertEquals(li.getDeletedInRecord().size(), 0); // accumulation of insertion Assert.assertEquals(li.getInsertedInRecord().size(), (indelPosition) ? coverage : 0); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getInsertedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Insertion); + } // check offsets of the insertion if (indelPosition) { Assert.assertEquals(li.getInsertedInRecord().get(0).getOffset(), 0); @@ -322,11 +362,19 @@ public void testStartWithSoftClipAndInsertion() { Assert.assertEquals(li.getPosition(), pos); // accumulation of coverage Assert.assertEquals(li.getRecordAndOffsets().size(), (indelPosition) ? 0 : coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), (indelPosition) ? 0 : coverage); // no accumulation of deletions Assert.assertEquals(li.getDeletedInRecord().size(), 0); // accumulation of insertion Assert.assertEquals(li.getInsertedInRecord().size(), (indelPosition) ? coverage : 0); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getInsertedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Insertion); + } // check offsets of the insertion if (indelPosition) { Assert.assertEquals(li.getInsertedInRecord().get(0).getOffset(), 1); @@ -367,11 +415,19 @@ public void testNBeforeInsertion() { Assert.assertEquals(li.getPosition(), pos); // accumulation of coverage Assert.assertEquals(li.getRecordAndOffsets().size(), (pos == endN) ? 0 : coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), (pos == endN) ? 0 : coverage); // no accumulation of deletions Assert.assertEquals(li.getDeletedInRecord().size(), 0); // accumulation of insertion Assert.assertEquals(li.getInsertedInRecord().size(), (pos == endN) ? coverage : 0); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getInsertedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Insertion); + } // check offsets of the insertion if (pos == endN) { Assert.assertEquals(li.getInsertedInRecord().get(0).getOffset(), 2); @@ -419,9 +475,17 @@ public void testNBeforeDeletion() { Assert.assertEquals(li.getPosition(), pos); // accumulation of coverage Assert.assertEquals(li.getRecordAndOffsets().size(), (insideDeletion) ? 0 : coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), coverage); // either will be all deletions, or all non-deletions, but always of size `coverage`. // accumulation of deletions Assert.assertEquals(li.getDeletedInRecord().size(), (insideDeletion) ? coverage : 0); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getDeletedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Deletion); + } // no accumulation of insertion Assert.assertEquals(li.getInsertedInRecord().size(), 0); // check offsets of the insertion @@ -498,6 +562,10 @@ public void testSimpleGappedAlignment() { if (incIndels && li.getPosition() == expectedInsertionPosition) { // check the accumulated coverage Assert.assertEquals(li.getInsertedInRecord().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getInsertedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Insertion); + } // check the record offset Assert.assertEquals(li.getInsertedInRecord().get(0).getOffset(), expectedInsertionOffset); Assert.assertEquals(li.getInsertedInRecord().get(1).getOffset(), expectedInsertionOffset); @@ -508,6 +576,10 @@ public void testSimpleGappedAlignment() { if (inDelRange) { // check the coverage for insertion and normal records Assert.assertEquals(li.getDeletedInRecord().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getDeletedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Deletion); + } Assert.assertEquals(li.getRecordAndOffsets().size(), 0); Assert.assertEquals(li.size(), coverage); // includes deletions // check the offset for the deletion @@ -516,6 +588,10 @@ public void testSimpleGappedAlignment() { } else { // if it is not a deletion, perform the same test as before Assert.assertEquals(li.getRecordAndOffsets().size(), coverage); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } Assert.assertEquals(li.size(), coverage); // Assert.assertEquals(li.getDeletedInRecord().size(), 0); Assert.assertEquals(li.getRecordAndOffsets().get(0).getOffset(), expectedReadOffsets[i]); @@ -582,6 +658,10 @@ public void testOverlappingGappedAlignmentsWithoutIndels() { Assert.assertEquals(li.size(), expectedDepths[i]); Assert.assertEquals(li.getPosition(), expectedReferencePositions[i]); Assert.assertEquals(li.getRecordAndOffsets().size(), expectedReadOffsets[i].length); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } for (int j = 0; j < expectedReadOffsets[i].length; ++j) { Assert.assertEquals(li.getRecordAndOffsets().get(j).getOffset(), expectedReadOffsets[i][j]); } @@ -658,11 +738,19 @@ public void testOverlappingGappedAlignmentsWithIndels() { Assert.assertEquals(li.size(), expectedDepths[i] + expectedDelDepths[i]); // include deletions Assert.assertEquals(li.getPosition(), expectedReferencePositions[i]); Assert.assertEquals(li.getRecordAndOffsets().size(), expectedReadOffsets[i].length); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getRecordAndOffsets()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Match); + } for (int j = 0; j < expectedReadOffsets[i].length; ++j) { Assert.assertEquals(li.getRecordAndOffsets().get(j).getOffset(), expectedReadOffsets[i][j]); } // check the deletions Assert.assertEquals(li.getDeletedInRecord().size(), expectedDelDepths[i]); + // Check the correct assignment of the alignment type + for (final SamLocusIterator.RecordAndOffset rao : li.getDeletedInRecord()) { + Assert.assertEquals(rao.getAlignmentType(), SamLocusIterator.RecordAndOffset.AlignmentType.Deletion); + } if (expectedDelDepths[i] != 0) { Assert.assertEquals(li.getDeletedInRecord().get(0).getOffset(), expectedDelOffset); }