From 3fe011a11cea0c6ddf35f8d9efa46ffadbe74a82 Mon Sep 17 00:00:00 2001
From: Jonn Smith
Date: Fri, 8 Nov 2024 15:54:32 -0500
Subject: [PATCH] Port of `CallableLoci` from GATK3 (#9031)
* This is a port of a tool from GATK which is being used in some active pipelines.
* CallableLoci is a a tool which calculates coverage based stats and identifies regions of the genome which should be "callable" or not based on several filters.
---
.../tools/walkers/coverage/CallableLoci.java | 429 ++++++++++++++++++
.../coverage/CallableLociIntegrationTest.java | 39 ++
...lable_loci.testBasicOperation.expected.bed | 422 +++++++++++++++++
...ci.testBasicOperation.expected.summary.txt | 7 +
4 files changed, 897 insertions(+)
create mode 100644 src/main/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLoci.java
create mode 100644 src/test/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLociIntegrationTest.java
create mode 100644 src/test/resources/callable_loci.testBasicOperation.expected.bed
create mode 100644 src/test/resources/callable_loci.testBasicOperation.expected.summary.txt
diff --git a/src/main/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLoci.java b/src/main/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLoci.java
new file mode 100644
index 00000000000..8432db16606
--- /dev/null
+++ b/src/main/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLoci.java
@@ -0,0 +1,429 @@
+package org.broadinstitute.hellbender.tools.walkers.coverage;
+
+
+import org.broadinstitute.barclay.argparser.Argument;
+import org.broadinstitute.barclay.argparser.CommandLineProgramProperties;
+import org.broadinstitute.barclay.argparser.Advanced;
+import org.broadinstitute.barclay.help.DocumentedFeature;
+import org.broadinstitute.barclay.argparser.ExperimentalFeature;
+import org.broadinstitute.hellbender.cmdline.StandardArgumentDefinitions;
+import org.broadinstitute.hellbender.cmdline.programgroups.CoverageAnalysisProgramGroup;
+import org.broadinstitute.hellbender.engine.FeatureContext;
+import org.broadinstitute.hellbender.engine.GATKPath;
+import org.broadinstitute.hellbender.engine.LocusWalker;
+import org.broadinstitute.hellbender.engine.ReferenceContext;
+import org.broadinstitute.hellbender.engine.filters.ReadFilter;
+import org.broadinstitute.hellbender.engine.filters.ReadFilterLibrary;
+import org.broadinstitute.hellbender.engine.filters.WellformedReadFilter;
+import org.broadinstitute.hellbender.engine.AlignmentContext;
+import org.broadinstitute.hellbender.exceptions.UserException;
+import org.broadinstitute.hellbender.utils.BaseUtils;
+import org.broadinstitute.hellbender.utils.SimpleInterval;
+import org.broadinstitute.hellbender.utils.pileup.PileupElement;
+import org.broadinstitute.hellbender.exceptions.GATKException;
+import htsjdk.samtools.util.Locatable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.stream.Collectors;
+import org.broadinstitute.hellbender.utils.Utils;
+import htsjdk.samtools.util.Lazy;
+import java.util.EnumMap;
+
+/**
+ * Collect statistics on callable, uncallable, poorly mapped, and other parts of the genome
+ *
+ *
+ * A very common question about a NGS set of reads is what areas of the genome are considered callable. This tool
+ * considers the coverage at each locus and emits either a per base state or a summary interval BED file that
+ * partitions the genomic intervals into the following callable states:
+ *
+ * - REF_N
+ * - The reference base was an N, which is not considered callable the GATK
+ * - PASS
+ * - The base satisfied the min. depth for calling but had less than maxDepth to avoid having EXCESSIVE_COVERAGE
+ * - NO_COVERAGE
+ * - Absolutely no reads were seen at this locus, regardless of the filtering parameters
+ * - LOW_COVERAGE
+ * - There were fewer than min. depth bases at the locus, after applying filters
+ * - EXCESSIVE_COVERAGE
+ * - More than --max-depth read at the locus, indicating some sort of mapping problem
+ * - POOR_MAPPING_QUALITY
+ * - More than --max-fraction-of-reads-with-low-mapq at the locus, indicating a poor mapping quality of the reads
+ *
+ *
+ *
+ * Input
+ *
+ * A BAM file containing exactly one sample.
+ *
+ *
+ * Output
+ *
+ * A file with the callable status covering each base and a table of callable status x count of all examined bases
+ *
+ * Usage example
+ *
+ * gatk CallableLoci \
+ * -I myreads.bam \
+ * -R myreference.fasta \
+ * -O callable_status.bed \
+ * --summary table.txt
+ *
+ *
+ * would produce a BED file that looks like:
+ *
+ * 20 10000000 10000864 PASS
+ * 20 10000865 10000985 POOR_MAPPING_QUALITY
+ * 20 10000986 10001138 PASS
+ * 20 10001139 10001254 POOR_MAPPING_QUALITY
+ * 20 10001255 10012255 PASS
+ * 20 10012256 10012259 POOR_MAPPING_QUALITY
+ * 20 10012260 10012263 PASS
+ * 20 10012264 10012328 POOR_MAPPING_QUALITY
+ * 20 10012329 10012550 PASS
+ * 20 10012551 10012551 LOW_COVERAGE
+ * 20 10012552 10012554 PASS
+ * 20 10012555 10012557 LOW_COVERAGE
+ * 20 10012558 10012558 PASS
+ *
+ * as well as a summary table that looks like:
+ *
+ *
+ * state nBases
+ * REF_N 0
+ * PASS 996046
+ * NO_COVERAGE 121
+ * LOW_COVERAGE 928
+ * EXCESSIVE_COVERAGE 0
+ * POOR_MAPPING_QUALITY 2906
+ *
+ *
+ * @author Mark DePristo / Jonn Smith
+ * @since May 7, 2010 / Nov 1, 2024
+ */
+@ExperimentalFeature
+@DocumentedFeature(groupName = "Coverage Analysis")
+@CommandLineProgramProperties(
+ summary = "Collect statistics on callable, uncallable, poorly mapped, and other parts of the genome",
+ oneLineSummary = "Determine callable status of loci",
+ programGroup = CoverageAnalysisProgramGroup.class
+)
+public class CallableLoci extends LocusWalker {
+
+ @Argument(fullName = StandardArgumentDefinitions.OUTPUT_LONG_NAME,
+ shortName = StandardArgumentDefinitions.OUTPUT_SHORT_NAME,
+ doc = "Output file (BED or per-base format)")
+ private GATKPath outputFile = null;
+
+ /**
+ * Callable loci summary counts will be written to this file.
+ */
+ @Argument(fullName = "summary",
+ doc = "Name of file for output summary")
+ private GATKPath summaryFile;
+
+ /**
+ * The gap between this value and mmq are reads that are not sufficiently well mapped for calling but
+ * aren't indicative of mapping problems. For example, if maxLowMAPQ = 1 and mmq = 20, then reads with
+ * MAPQ == 0 are poorly mapped, MAPQ >= 20 are considered as contributing to calling, where
+ * reads with MAPQ >= 1 and < 20 are not bad in and of themselves but aren't sufficiently good to contribute to
+ * calling. In effect this reads are invisible, driving the base to the NO_ or LOW_COVERAGE states
+ */
+ @Argument(fullName = "max-low-mapq", shortName = "mlmq", minValue = 0, maxValue = 255, optional = true,
+ doc = "Maximum value for MAPQ to be considered a problematic mapped read")
+ private int maxLowMAPQ = 1;
+
+ /**
+ * Reads with MAPQ > minMappingQuality are treated as usable for variation detection, contributing to the PASS
+ * state.
+ */
+ @Argument(fullName = "min-mapping-quality", shortName = "mmq", minValue = 0, maxValue = 255, optional = true,
+ doc = "Minimum mapping quality of reads to count towards depth")
+ private int minMappingQuality = 10;
+
+ /**
+ * Bases with less than minBaseQuality are viewed as not sufficiently high quality to contribute to the PASS state
+ */
+ @Argument(fullName = "min-base-quality", shortName = "mbq", minValue = 0, maxValue = 255, optional = true,
+ doc = "Minimum quality of bases to count towards depth")
+ private int minBaseQuality = 20;
+
+ /**
+ * If the number of QC+ bases (on reads with MAPQ > minMappingQuality and with base quality > minBaseQuality) exceeds this
+ * value and is less than maxDepth the site is considered PASS.
+ */
+ @Advanced
+ @Argument(fullName = "min-depth", shortName = "min-depth", minValue = 0, optional = true,
+ doc = "Minimum QC+ read depth before a locus is considered callable")
+ private int minDepth = 4;
+
+ /**
+ * If the QC+ depth exceeds this value the site is considered to have EXCESSIVE_DEPTH
+ */
+ @Argument(fullName = "max-depth", shortName = "max-depth", optional = true,
+ doc = "Maximum read depth before a locus is considered poorly mapped")
+ private Integer maxDepth = null;
+
+ /**
+ * We don't want to consider a site as POOR_MAPPING_QUALITY just because it has two reads, and one is MAPQ. We
+ * won't assign a site to the POOR_MAPPING_QUALITY state unless there are at least minDepthForLowMAPQ reads
+ * covering the site.
+ */
+ @Advanced
+ @Argument(fullName = "min-depth-for-low-mapq", shortName = "mdflmq", optional = true,
+ doc = "Minimum read depth before a locus is considered a potential candidate for poorly mapped")
+ private int minDepthLowMAPQ = 10;
+
+ /**
+ * If the number of reads at this site is greater than minDepthForLowMAPQ and the fraction of reads with low mapping quality
+ * exceeds this fraction then the site has POOR_MAPPING_QUALITY.
+ */
+ @Argument(fullName = "max-fraction-of-reads-with-low-mapq", shortName = "frlmq", optional = true,
+ doc = "If the fraction of reads at a base with low mapping quality exceeds this value, the site may be poorly mapped")
+ private double maxLowMAPQFraction = 0.1;
+
+ /**
+ * The output of this tool will be written in this format. The recommended option is BED.
+ */
+ @Advanced
+ @Argument(fullName = "format", shortName = "format", optional = true,
+ doc = "Output format")
+ private OutputFormat outputFormat = OutputFormat.BED;
+
+ private OutputStreamWriter outputStream = null;
+ private OutputStreamWriter summaryStream = null;
+
+ public enum OutputFormat {
+ BED,
+ STATE_PER_BASE
+ }
+
+ public enum State {
+ REF_N,
+ CALLABLE,
+ NO_COVERAGE,
+ LOW_COVERAGE,
+ EXCESSIVE_COVERAGE,
+ POOR_MAPPING_QUALITY
+ }
+
+ private BaseState currentState = null;
+ private final StateCounter stateCounter = new StateCounter();
+
+ protected static class BaseState implements Locatable {
+ private Locatable interval;
+ private final State state;
+
+ public BaseState(Locatable interval, State state) {
+ this.interval = interval;
+ this.state = state;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ @Override
+ public String getContig() {
+ return interval.getContig();
+ }
+
+ @Override
+ public int getStart() {
+ return interval.getStart();
+ }
+
+ @Override
+ public int getEnd() {
+ return interval.getEnd();
+ }
+
+ public void updateInterval(final Locatable newInterval) {
+ this.interval = new SimpleInterval(
+ interval.getContig(),
+ interval.getStart(),
+ newInterval.getEnd()
+ );
+ }
+
+ @Override
+ public String toString() {
+ return toBedString();
+ }
+
+ public String toBedString() {
+ // BED format is 0-based, so subtract 1 from start
+ return String.format("%s\t%d\t%d\t%s",
+ interval.getContig(),
+ interval.getStart() - 1,
+ interval.getEnd(),
+ state);
+ }
+ }
+
+ @Override
+ public boolean requiresReference() {
+ return true;
+ }
+
+ @Override
+ public boolean emitEmptyLoci() {
+ return true;
+ }
+
+ @Override
+ public boolean includeNs() {
+ return true;
+ }
+
+ @Override
+ // This is the default set of filters for CallableLoci as implemented in the GATK3 version of this tool.
+ public List getDefaultReadFilters() {
+ final List defaultFilters = new ArrayList<>(6);
+ defaultFilters.add(new ReadFilterLibrary.GoodCigarReadFilter());
+ defaultFilters.add(new ReadFilterLibrary.NotDuplicateReadFilter());
+ defaultFilters.add(new ReadFilterLibrary.PassesVendorQualityCheckReadFilter());
+ defaultFilters.add(new WellformedReadFilter());
+ defaultFilters.add(new ReadFilterLibrary.PrimaryLineReadFilter());
+ defaultFilters.add(new ReadFilterLibrary.MappedReadFilter());
+ return defaultFilters;
+ }
+
+ @Override
+ public void onTraversalStart() {
+
+ // Validate sample count
+ final List sampleList = getHeaderForReads().getReadGroups().stream()
+ .map(rg -> rg.getSample())
+ .distinct()
+ .collect(Collectors.toList());
+
+ if (sampleList.size() != 1) {
+ throw new UserException.BadInput("CallableLoci only works for a single sample. Found " + sampleList.size() + " samples (" + String.join(", ", sampleList) + ").");
+ }
+
+ outputStream = new OutputStreamWriter(outputFile.getOutputStream());
+ summaryStream = new OutputStreamWriter(summaryFile.getOutputStream());
+ }
+
+ private State getCurrentState(ReferenceContext referenceContext, AlignmentContext alignmentContext) {
+ State state;
+
+ if (BaseUtils.isNBase(referenceContext.getBase())) {
+ state = State.REF_N;
+ } else {
+ int rawDepth = 0, QCDepth = 0, lowMAPQDepth = 0;
+
+ for (PileupElement e : alignmentContext.getBasePileup()) {
+ rawDepth++;
+
+ if (e.getMappingQual() <= maxLowMAPQ) {
+ lowMAPQDepth++;
+ }
+
+ if (e.getMappingQual() >= minMappingQuality && (e.getQual() >= minBaseQuality || e.isDeletion())) {
+ QCDepth++;
+ }
+ }
+
+ if (rawDepth == 0) {
+ state = State.NO_COVERAGE;
+ } else if ((rawDepth >= minDepthLowMAPQ) && (((double)lowMAPQDepth) / ((double)rawDepth) >= maxLowMAPQFraction)) {
+ state = State.POOR_MAPPING_QUALITY;
+ } else if (QCDepth < minDepth) {
+ state = State.LOW_COVERAGE;
+ } else if (maxDepth != null && rawDepth >= maxDepth) {
+ state = State.EXCESSIVE_COVERAGE;
+ } else {
+ state = State.CALLABLE;
+ }
+ }
+
+ return state;
+ }
+
+ @Override
+ public void apply(AlignmentContext alignmentContext, ReferenceContext referenceContext, FeatureContext featureContext) {
+
+ final State state = getCurrentState(referenceContext, alignmentContext);
+ BaseState callableState = new BaseState(alignmentContext, state);
+
+ // Update counts using the counter
+ stateCounter.increment(state);
+
+ try {
+ if (outputFormat == OutputFormat.STATE_PER_BASE) {
+ outputStream.write(callableState.toBedString() + "\n");
+ } else {
+ // BED format - integrate adjacent regions with same state
+ if (currentState == null) {
+ currentState = callableState;
+ } else if (callableState.getStart() != currentState.getEnd() + 1 || currentState.getState() != callableState.getState()) {
+ outputStream.write(currentState.toBedString() + "\n");
+ currentState = callableState;
+ } else {
+ currentState.updateInterval(callableState);
+ }
+ }
+ } catch (IOException e) {
+ throw new GATKException("Error writing to output stream", e);
+ }
+ }
+
+ @Override
+ public Object onTraversalSuccess() {
+
+ try{
+ // Print final state for BED format
+ if (outputFormat == OutputFormat.BED && currentState != null) {
+ outputStream.write(currentState.toBedString() + "\n");
+ }
+
+ // Write summary statistics using the counter
+ summaryStream.write(String.format("%30s %s%n", "state", "nBases"));
+ for (State state : State.values()) {
+ summaryStream.write(String.format("%30s %d%n", state, stateCounter.getCount(state)));
+ }
+
+ } catch (IOException e) {
+ throw new GATKException("Error writing to summary stream", e);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void closeTool() {
+ try {
+ outputStream.close();
+ summaryStream.close();
+ } catch (IOException e) {
+ throw new GATKException("Error closing output streams", e);
+ }
+ }
+
+ private static class StateCounter {
+ private final EnumMap counts = new EnumMap<>(State.class);
+
+ public StateCounter() {
+ for (State state : State.values()) {
+ counts.put(state, 0L);
+ }
+ }
+
+ public void increment(State state) {
+ counts.put(state, counts.get(state) + 1);
+ }
+
+ public long getCount(State state) {
+ return counts.get(state);
+ }
+
+ public EnumMap getCounts() {
+ return new EnumMap<>(counts);
+ }
+ }
+}
diff --git a/src/test/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLociIntegrationTest.java b/src/test/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLociIntegrationTest.java
new file mode 100644
index 00000000000..3346387d027
--- /dev/null
+++ b/src/test/java/org/broadinstitute/hellbender/tools/walkers/coverage/CallableLociIntegrationTest.java
@@ -0,0 +1,39 @@
+package org.broadinstitute.hellbender.tools.walkers.coverage;
+
+import org.broadinstitute.hellbender.CommandLineProgramTest;
+import org.broadinstitute.hellbender.testutils.ArgumentsBuilder;
+import org.broadinstitute.hellbender.testutils.IntegrationTestSpec;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.util.Arrays;
+
+public class CallableLociIntegrationTest extends CommandLineProgramTest {
+
+ public static final String testDataDir = publicTestDir;
+
+ @Test
+ public void testBasicOperation() throws Exception {
+ final File outputFile = createTempFile("callableLoci", ".bed");
+ final File summaryFile = createTempFile("callableLoci", ".summary.txt");
+
+ final ArgumentsBuilder args = new ArgumentsBuilder();
+ args.addInput(new File(largeFileTestDir + "CEUTrio.HiSeq.WGS.b37.NA12878.20.21.bam"))
+ .addReference(new File(b37_reference_20_21))
+ .addOutput(outputFile)
+ .add("summary", summaryFile)
+ .add("format", "BED");
+
+ runCommandLine(args);
+
+ IntegrationTestSpec.assertEqualTextFiles(
+ outputFile,
+ new File(testDataDir + "callable_loci.testBasicOperation.expected.bed"),
+ "#");
+
+ IntegrationTestSpec.assertEqualTextFiles(
+ summaryFile,
+ new File(testDataDir + "callable_loci.testBasicOperation.expected.summary.txt"),
+ "#");
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/callable_loci.testBasicOperation.expected.bed b/src/test/resources/callable_loci.testBasicOperation.expected.bed
new file mode 100644
index 00000000000..055d22443c9
--- /dev/null
+++ b/src/test/resources/callable_loci.testBasicOperation.expected.bed
@@ -0,0 +1,422 @@
+20 0 60000 REF_N
+20 60000 9999901 NO_COVERAGE
+20 9999901 9999904 LOW_COVERAGE
+20 9999904 10000862 CALLABLE
+20 10000862 10000983 POOR_MAPPING_QUALITY
+20 10000983 10001000 CALLABLE
+20 10001000 10001003 POOR_MAPPING_QUALITY
+20 10001003 10001006 CALLABLE
+20 10001006 10001010 POOR_MAPPING_QUALITY
+20 10001010 10001074 CALLABLE
+20 10001074 10001254 POOR_MAPPING_QUALITY
+20 10001254 10015586 CALLABLE
+20 10015586 10015599 LOW_COVERAGE
+20 10015599 10098273 CALLABLE
+20 10098273 10098279 LOW_COVERAGE
+20 10098279 10098288 CALLABLE
+20 10098288 10098327 LOW_COVERAGE
+20 10098327 10098345 CALLABLE
+20 10098345 10098495 LOW_COVERAGE
+20 10098495 10098551 NO_COVERAGE
+20 10098551 10098685 LOW_COVERAGE
+20 10098685 10098689 CALLABLE
+20 10098689 10098690 LOW_COVERAGE
+20 10098690 10130790 CALLABLE
+20 10130790 10130886 POOR_MAPPING_QUALITY
+20 10130886 10199097 CALLABLE
+20 10199097 10199102 LOW_COVERAGE
+20 10199102 10199105 CALLABLE
+20 10199105 10199106 LOW_COVERAGE
+20 10199106 10199109 CALLABLE
+20 10199109 10199110 LOW_COVERAGE
+20 10199110 10199111 CALLABLE
+20 10199111 10199112 LOW_COVERAGE
+20 10199112 10199114 CALLABLE
+20 10199114 10199115 LOW_COVERAGE
+20 10199115 10199125 CALLABLE
+20 10199125 10199126 LOW_COVERAGE
+20 10199126 10199133 CALLABLE
+20 10199133 10199138 LOW_COVERAGE
+20 10199138 10199139 CALLABLE
+20 10199139 10199140 LOW_COVERAGE
+20 10199140 10199141 CALLABLE
+20 10199141 10199142 LOW_COVERAGE
+20 10199142 10199146 CALLABLE
+20 10199146 10199147 LOW_COVERAGE
+20 10199147 10199165 CALLABLE
+20 10199165 10199166 LOW_COVERAGE
+20 10199166 10199170 CALLABLE
+20 10199170 10199176 LOW_COVERAGE
+20 10199176 10199190 CALLABLE
+20 10199190 10199191 LOW_COVERAGE
+20 10199191 10199196 CALLABLE
+20 10199196 10199222 LOW_COVERAGE
+20 10199222 10199223 CALLABLE
+20 10199223 10199224 LOW_COVERAGE
+20 10199224 10199228 CALLABLE
+20 10199228 10199229 LOW_COVERAGE
+20 10199229 10199233 CALLABLE
+20 10199233 10199396 LOW_COVERAGE
+20 10199396 10250098 CALLABLE
+20 10250098 10250100 LOW_COVERAGE
+20 10250100 26319569 NO_COVERAGE
+20 26319569 29419569 REF_N
+20 29419569 29653908 NO_COVERAGE
+20 29653908 29803908 REF_N
+20 29803908 34897085 NO_COVERAGE
+20 34897085 34947085 REF_N
+20 34947085 61091437 NO_COVERAGE
+20 61091437 61141437 REF_N
+20 61141437 61213369 NO_COVERAGE
+20 61213369 61263369 REF_N
+20 61263369 62965520 NO_COVERAGE
+20 62965520 63025520 REF_N
+21 0 9411193 REF_N
+21 9411193 9595548 NO_COVERAGE
+21 9595548 9645548 REF_N
+21 9645548 9775437 NO_COVERAGE
+21 9775437 9825437 REF_N
+21 9825437 9999900 NO_COVERAGE
+21 9999900 9999906 LOW_COVERAGE
+21 9999906 10001414 POOR_MAPPING_QUALITY
+21 10001414 10001421 CALLABLE
+21 10001421 10004231 POOR_MAPPING_QUALITY
+21 10004231 10004255 CALLABLE
+21 10004255 10008552 POOR_MAPPING_QUALITY
+21 10008552 10008580 CALLABLE
+21 10008580 10009276 POOR_MAPPING_QUALITY
+21 10009276 10009375 CALLABLE
+21 10009375 10011967 POOR_MAPPING_QUALITY
+21 10011967 10012000 CALLABLE
+21 10012000 10012283 POOR_MAPPING_QUALITY
+21 10012283 10012313 CALLABLE
+21 10012313 10012505 POOR_MAPPING_QUALITY
+21 10012505 10012608 CALLABLE
+21 10012608 10012886 POOR_MAPPING_QUALITY
+21 10012886 10012910 CALLABLE
+21 10012910 10013107 POOR_MAPPING_QUALITY
+21 10013107 10013108 CALLABLE
+21 10013108 10013312 POOR_MAPPING_QUALITY
+21 10013312 10013362 CALLABLE
+21 10013362 10013598 POOR_MAPPING_QUALITY
+21 10013598 10013703 CALLABLE
+21 10013703 10013723 POOR_MAPPING_QUALITY
+21 10013723 10013724 CALLABLE
+21 10013724 10013725 POOR_MAPPING_QUALITY
+21 10013725 10013758 CALLABLE
+21 10013758 10014526 POOR_MAPPING_QUALITY
+21 10014526 10014557 CALLABLE
+21 10014557 10015174 POOR_MAPPING_QUALITY
+21 10015174 10015243 CALLABLE
+21 10015243 10015680 POOR_MAPPING_QUALITY
+21 10015680 10015682 CALLABLE
+21 10015682 10016888 POOR_MAPPING_QUALITY
+21 10016888 10016891 CALLABLE
+21 10016891 10018234 POOR_MAPPING_QUALITY
+21 10018234 10018282 CALLABLE
+21 10018282 10018667 POOR_MAPPING_QUALITY
+21 10018667 10018740 CALLABLE
+21 10018740 10019065 POOR_MAPPING_QUALITY
+21 10019065 10019144 CALLABLE
+21 10019144 10019342 POOR_MAPPING_QUALITY
+21 10019342 10019357 CALLABLE
+21 10019357 10020750 POOR_MAPPING_QUALITY
+21 10020750 10020759 CALLABLE
+21 10020759 10021272 POOR_MAPPING_QUALITY
+21 10021272 10021279 CALLABLE
+21 10021279 10021908 POOR_MAPPING_QUALITY
+21 10021908 10021915 CALLABLE
+21 10021915 10022538 POOR_MAPPING_QUALITY
+21 10022538 10022548 CALLABLE
+21 10022548 10023446 POOR_MAPPING_QUALITY
+21 10023446 10023456 CALLABLE
+21 10023456 10024149 POOR_MAPPING_QUALITY
+21 10024149 10024159 CALLABLE
+21 10024159 10026817 POOR_MAPPING_QUALITY
+21 10026817 10026831 CALLABLE
+21 10026831 10026952 POOR_MAPPING_QUALITY
+21 10026952 10026954 CALLABLE
+21 10026954 10027140 POOR_MAPPING_QUALITY
+21 10027140 10027142 CALLABLE
+21 10027142 10027311 POOR_MAPPING_QUALITY
+21 10027311 10027405 CALLABLE
+21 10027405 10027537 POOR_MAPPING_QUALITY
+21 10027537 10027545 CALLABLE
+21 10027545 10029107 POOR_MAPPING_QUALITY
+21 10029107 10029120 CALLABLE
+21 10029120 10029401 POOR_MAPPING_QUALITY
+21 10029401 10029428 CALLABLE
+21 10029428 10029429 POOR_MAPPING_QUALITY
+21 10029429 10029436 CALLABLE
+21 10029436 10029783 POOR_MAPPING_QUALITY
+21 10029783 10029786 CALLABLE
+21 10029786 10029787 POOR_MAPPING_QUALITY
+21 10029787 10029819 CALLABLE
+21 10029819 10030539 POOR_MAPPING_QUALITY
+21 10030539 10030590 CALLABLE
+21 10030590 10031708 POOR_MAPPING_QUALITY
+21 10031708 10031717 CALLABLE
+21 10031717 10034154 POOR_MAPPING_QUALITY
+21 10034154 10034167 CALLABLE
+21 10034167 10034359 POOR_MAPPING_QUALITY
+21 10034359 10034363 CALLABLE
+21 10034363 10034666 POOR_MAPPING_QUALITY
+21 10034666 10034674 CALLABLE
+21 10034674 10034915 POOR_MAPPING_QUALITY
+21 10034915 10034920 LOW_COVERAGE
+21 10034920 10084920 REF_N
+21 10084920 10085125 CALLABLE
+21 10085125 10085196 POOR_MAPPING_QUALITY
+21 10085196 10085482 CALLABLE
+21 10085482 10085486 POOR_MAPPING_QUALITY
+21 10085486 10085493 CALLABLE
+21 10085493 10085510 POOR_MAPPING_QUALITY
+21 10085510 10085511 CALLABLE
+21 10085511 10085548 POOR_MAPPING_QUALITY
+21 10085548 10086183 CALLABLE
+21 10086183 10086211 POOR_MAPPING_QUALITY
+21 10086211 10086230 CALLABLE
+21 10086230 10086233 POOR_MAPPING_QUALITY
+21 10086233 10086375 CALLABLE
+21 10086375 10086528 POOR_MAPPING_QUALITY
+21 10086528 10086630 CALLABLE
+21 10086630 10087011 POOR_MAPPING_QUALITY
+21 10087011 10087068 CALLABLE
+21 10087068 10088336 POOR_MAPPING_QUALITY
+21 10088336 10088340 LOW_COVERAGE
+21 10088340 10092857 POOR_MAPPING_QUALITY
+21 10092857 10092871 CALLABLE
+21 10092871 10094124 POOR_MAPPING_QUALITY
+21 10094124 10094126 LOW_COVERAGE
+21 10094126 10100485 POOR_MAPPING_QUALITY
+21 10100485 10100491 LOW_COVERAGE
+21 10100491 10100634 POOR_MAPPING_QUALITY
+21 10100634 10100645 LOW_COVERAGE
+21 10100645 10103150 POOR_MAPPING_QUALITY
+21 10103150 10103156 LOW_COVERAGE
+21 10103156 10103157 NO_COVERAGE
+21 10103157 10103160 LOW_COVERAGE
+21 10103160 10107501 POOR_MAPPING_QUALITY
+21 10107501 10107504 LOW_COVERAGE
+21 10107504 10108423 POOR_MAPPING_QUALITY
+21 10108423 10108425 LOW_COVERAGE
+21 10108425 10108428 POOR_MAPPING_QUALITY
+21 10108428 10108429 LOW_COVERAGE
+21 10108429 10110001 POOR_MAPPING_QUALITY
+21 10110001 10110007 LOW_COVERAGE
+21 10110007 10113883 POOR_MAPPING_QUALITY
+21 10113883 10113888 LOW_COVERAGE
+21 10113888 10113891 NO_COVERAGE
+21 10113891 10113909 LOW_COVERAGE
+21 10113909 10114200 POOR_MAPPING_QUALITY
+21 10114200 10114213 LOW_COVERAGE
+21 10114213 10118461 POOR_MAPPING_QUALITY
+21 10118461 10118466 LOW_COVERAGE
+21 10118466 10121719 POOR_MAPPING_QUALITY
+21 10121719 10121744 LOW_COVERAGE
+21 10121744 10122767 POOR_MAPPING_QUALITY
+21 10122767 10122774 LOW_COVERAGE
+21 10122774 10124504 POOR_MAPPING_QUALITY
+21 10124504 10124513 LOW_COVERAGE
+21 10124513 10124772 POOR_MAPPING_QUALITY
+21 10124772 10124774 CALLABLE
+21 10124774 10127843 POOR_MAPPING_QUALITY
+21 10127843 10127895 CALLABLE
+21 10127895 10129890 POOR_MAPPING_QUALITY
+21 10129890 10129926 CALLABLE
+21 10129926 10131172 POOR_MAPPING_QUALITY
+21 10131172 10131185 CALLABLE
+21 10131185 10132125 POOR_MAPPING_QUALITY
+21 10132125 10132147 LOW_COVERAGE
+21 10132147 10134066 POOR_MAPPING_QUALITY
+21 10134066 10134072 CALLABLE
+21 10134072 10136047 POOR_MAPPING_QUALITY
+21 10136047 10136097 CALLABLE
+21 10136097 10136560 POOR_MAPPING_QUALITY
+21 10136560 10136562 CALLABLE
+21 10136562 10136743 POOR_MAPPING_QUALITY
+21 10136743 10136765 CALLABLE
+21 10136765 10137054 POOR_MAPPING_QUALITY
+21 10137054 10137087 CALLABLE
+21 10137087 10137327 POOR_MAPPING_QUALITY
+21 10137327 10137357 CALLABLE
+21 10137357 10137582 POOR_MAPPING_QUALITY
+21 10137582 10137588 CALLABLE
+21 10137588 10137803 POOR_MAPPING_QUALITY
+21 10137803 10137808 CALLABLE
+21 10137808 10138250 POOR_MAPPING_QUALITY
+21 10138250 10138252 CALLABLE
+21 10138252 10138860 POOR_MAPPING_QUALITY
+21 10138860 10138928 CALLABLE
+21 10138928 10140888 POOR_MAPPING_QUALITY
+21 10140888 10140933 CALLABLE
+21 10140933 10141123 POOR_MAPPING_QUALITY
+21 10141123 10141250 CALLABLE
+21 10141250 10141280 POOR_MAPPING_QUALITY
+21 10141280 10141286 CALLABLE
+21 10141286 10141585 POOR_MAPPING_QUALITY
+21 10141585 10141587 CALLABLE
+21 10141587 10144390 POOR_MAPPING_QUALITY
+21 10144390 10144412 CALLABLE
+21 10144412 10145592 POOR_MAPPING_QUALITY
+21 10145592 10145594 LOW_COVERAGE
+21 10145594 10145595 NO_COVERAGE
+21 10145595 10145599 LOW_COVERAGE
+21 10145599 10147275 POOR_MAPPING_QUALITY
+21 10147275 10147277 CALLABLE
+21 10147277 10147285 POOR_MAPPING_QUALITY
+21 10147285 10147304 CALLABLE
+21 10147304 10147916 POOR_MAPPING_QUALITY
+21 10147916 10147917 LOW_COVERAGE
+21 10147917 10148437 POOR_MAPPING_QUALITY
+21 10148437 10148446 CALLABLE
+21 10148446 10150831 POOR_MAPPING_QUALITY
+21 10150831 10150840 CALLABLE
+21 10150840 10151788 POOR_MAPPING_QUALITY
+21 10151788 10151790 CALLABLE
+21 10151790 10153161 POOR_MAPPING_QUALITY
+21 10153161 10153175 CALLABLE
+21 10153175 10153853 POOR_MAPPING_QUALITY
+21 10153853 10153855 LOW_COVERAGE
+21 10153855 10155312 POOR_MAPPING_QUALITY
+21 10155312 10155323 LOW_COVERAGE
+21 10155323 10155683 POOR_MAPPING_QUALITY
+21 10155683 10155688 CALLABLE
+21 10155688 10156051 POOR_MAPPING_QUALITY
+21 10156051 10156055 CALLABLE
+21 10156055 10157124 POOR_MAPPING_QUALITY
+21 10157124 10157125 CALLABLE
+21 10157125 10158275 POOR_MAPPING_QUALITY
+21 10158275 10158281 LOW_COVERAGE
+21 10158281 10158282 NO_COVERAGE
+21 10158282 10158287 LOW_COVERAGE
+21 10158287 10160175 POOR_MAPPING_QUALITY
+21 10160175 10160178 LOW_COVERAGE
+21 10160178 10164945 POOR_MAPPING_QUALITY
+21 10164945 10164950 CALLABLE
+21 10164950 10165347 POOR_MAPPING_QUALITY
+21 10165347 10165361 CALLABLE
+21 10165361 10165647 POOR_MAPPING_QUALITY
+21 10165647 10165650 CALLABLE
+21 10165650 10166922 POOR_MAPPING_QUALITY
+21 10166922 10166927 CALLABLE
+21 10166927 10174185 POOR_MAPPING_QUALITY
+21 10174185 10174189 CALLABLE
+21 10174189 10174826 POOR_MAPPING_QUALITY
+21 10174826 10174830 CALLABLE
+21 10174830 10175880 POOR_MAPPING_QUALITY
+21 10175880 10175885 CALLABLE
+21 10175885 10176078 POOR_MAPPING_QUALITY
+21 10176078 10176079 LOW_COVERAGE
+21 10176079 10177872 POOR_MAPPING_QUALITY
+21 10177872 10177877 CALLABLE
+21 10177877 10178838 POOR_MAPPING_QUALITY
+21 10178838 10178843 LOW_COVERAGE
+21 10178843 10179300 POOR_MAPPING_QUALITY
+21 10179300 10179308 CALLABLE
+21 10179308 10179344 POOR_MAPPING_QUALITY
+21 10179344 10179354 CALLABLE
+21 10179354 10181387 POOR_MAPPING_QUALITY
+21 10181387 10181389 CALLABLE
+21 10181389 10182971 POOR_MAPPING_QUALITY
+21 10182971 10182975 CALLABLE
+21 10182975 10183371 POOR_MAPPING_QUALITY
+21 10183371 10183381 CALLABLE
+21 10183381 10184122 POOR_MAPPING_QUALITY
+21 10184122 10184130 CALLABLE
+21 10184130 10185818 POOR_MAPPING_QUALITY
+21 10185818 10185820 CALLABLE
+21 10185820 10186268 POOR_MAPPING_QUALITY
+21 10186268 10186286 CALLABLE
+21 10186286 10186356 POOR_MAPPING_QUALITY
+21 10186356 10186364 CALLABLE
+21 10186364 10186900 POOR_MAPPING_QUALITY
+21 10186900 10186909 CALLABLE
+21 10186909 10187286 POOR_MAPPING_QUALITY
+21 10187286 10187356 CALLABLE
+21 10187356 10187389 POOR_MAPPING_QUALITY
+21 10187389 10187398 CALLABLE
+21 10187398 10188005 POOR_MAPPING_QUALITY
+21 10188005 10188011 CALLABLE
+21 10188011 10189330 POOR_MAPPING_QUALITY
+21 10189330 10189342 CALLABLE
+21 10189342 10191035 POOR_MAPPING_QUALITY
+21 10191035 10191046 LOW_COVERAGE
+21 10191046 10191652 POOR_MAPPING_QUALITY
+21 10191652 10191654 CALLABLE
+21 10191654 10192714 POOR_MAPPING_QUALITY
+21 10192714 10192718 CALLABLE
+21 10192718 10192814 POOR_MAPPING_QUALITY
+21 10192814 10192815 CALLABLE
+21 10192815 10198849 POOR_MAPPING_QUALITY
+21 10198849 10198850 CALLABLE
+21 10198850 10199745 POOR_MAPPING_QUALITY
+21 10199745 10199748 LOW_COVERAGE
+21 10199748 10199815 POOR_MAPPING_QUALITY
+21 10199815 10199816 CALLABLE
+21 10199816 10199821 POOR_MAPPING_QUALITY
+21 10199821 10199826 CALLABLE
+21 10199826 10200721 POOR_MAPPING_QUALITY
+21 10200721 10200740 CALLABLE
+21 10200740 10200950 POOR_MAPPING_QUALITY
+21 10200950 10201039 CALLABLE
+21 10201039 10201248 POOR_MAPPING_QUALITY
+21 10201248 10201323 CALLABLE
+21 10201323 10201929 POOR_MAPPING_QUALITY
+21 10201929 10201935 CALLABLE
+21 10201935 10202412 POOR_MAPPING_QUALITY
+21 10202412 10202423 CALLABLE
+21 10202423 10203782 POOR_MAPPING_QUALITY
+21 10203782 10203795 CALLABLE
+21 10203795 10205580 POOR_MAPPING_QUALITY
+21 10205580 10205647 CALLABLE
+21 10205647 10208178 POOR_MAPPING_QUALITY
+21 10208178 10208238 CALLABLE
+21 10208238 10208952 POOR_MAPPING_QUALITY
+21 10208952 10208953 CALLABLE
+21 10208953 10208954 POOR_MAPPING_QUALITY
+21 10208954 10208956 CALLABLE
+21 10208956 10209828 POOR_MAPPING_QUALITY
+21 10209828 10209835 CALLABLE
+21 10209835 10209956 POOR_MAPPING_QUALITY
+21 10209956 10210047 CALLABLE
+21 10210047 10210393 POOR_MAPPING_QUALITY
+21 10210393 10210401 CALLABLE
+21 10210401 10212341 POOR_MAPPING_QUALITY
+21 10212341 10212342 CALLABLE
+21 10212342 10212438 POOR_MAPPING_QUALITY
+21 10212438 10212448 CALLABLE
+21 10212448 10212848 POOR_MAPPING_QUALITY
+21 10212848 10212850 CALLABLE
+21 10212850 10214810 POOR_MAPPING_QUALITY
+21 10214810 10214812 CALLABLE
+21 10214812 10215611 POOR_MAPPING_QUALITY
+21 10215611 10215615 CALLABLE
+21 10215615 10215950 POOR_MAPPING_QUALITY
+21 10215950 10215972 LOW_COVERAGE
+21 10215972 10215976 NO_COVERAGE
+21 10215976 10365976 REF_N
+21 10365976 10647896 NO_COVERAGE
+21 10647896 10697896 REF_N
+21 10697896 11188129 NO_COVERAGE
+21 11188129 14338129 REF_N
+21 14338129 33157035 NO_COVERAGE
+21 33157035 33157055 REF_N
+21 33157055 33157379 NO_COVERAGE
+21 33157379 33157389 REF_N
+21 33157389 40285944 NO_COVERAGE
+21 40285944 40285954 REF_N
+21 40285954 42955559 NO_COVERAGE
+21 42955559 43005559 REF_N
+21 43005559 43226828 NO_COVERAGE
+21 43226828 43227328 REF_N
+21 43227328 43249342 NO_COVERAGE
+21 43249342 43250842 REF_N
+21 43250842 44035894 NO_COVERAGE
+21 44035894 44035904 REF_N
+21 44035904 44632664 NO_COVERAGE
+21 44632664 44682664 REF_N
+21 44682664 44888040 NO_COVERAGE
+21 44888040 44888050 REF_N
+21 44888050 48119895 NO_COVERAGE
+21 48119895 48129895 REF_N
diff --git a/src/test/resources/callable_loci.testBasicOperation.expected.summary.txt b/src/test/resources/callable_loci.testBasicOperation.expected.summary.txt
new file mode 100644
index 00000000000..910c5d2771a
--- /dev/null
+++ b/src/test/resources/callable_loci.testBasicOperation.expected.summary.txt
@@ -0,0 +1,7 @@
+ state nBases
+ REF_N 16543253
+ CALLABLE 253137
+ NO_COVERAGE 94195953
+ LOW_COVERAGE 800
+ EXCESSIVE_COVERAGE 0
+ POOR_MAPPING_QUALITY 162272