Skip to content

Consensus Model

Aleksey Ermolaev edited this page Oct 20, 2026 · 2 revisions

Consensus model

Consensus is generated per sample over every position of that sample's coverage BED. The model is caller-order independent by default and supports configurable absence, filtered-record and tie policies. Parameters are listed in Configuration; output fields are described in Output structure.

Caller input normalisation

  • Each caller BCF is normalised with bcftools norm -f ref -m -any --atomize.
  • Multi-allelic records are split into one record per ALT, and complex alleles are atomised.
  • Each record contributes votes for its own (REF, ALT) alleles. Atomised records with different REF alleles at the same position (for example a SNP A>G and a deletion AT>A) remain distinct.
  • Split records whose ALT copies exceed the caller's ploidy are inconsistent (for example 1/1 plus 0/1 in a diploid) and make that caller abstain.
  • Alleles are keyed by REF and ALT sequences, so deletions that share an anchor ALT but have different REF lengths are distinct candidates.
  • Low-quality records are kept with FILTER=LowQual instead of being removed. Under the default consensus.filtered_vote: "abstain", a caller that saw a variant cannot be mistaken for a reference vote.

Two-stage voting

  1. An ALT allele becomes a candidate when at least consensus.threshold callers support it.
  2. Among the supporters of a candidate, the majority zygosity (heterozygous 0/1 versus homozygous ALT 1/1) decides the consensus genotype.

consensus.tie_policy resolves a zygosity tie:

  • "nocall" (default): the position is a no-call, independent of caller order.
  • "first": the earliest supporter in the configured caller order decides.

When several ALT alleles reach the threshold, each becomes its own split consensus record. The records are re-joined for the final output unless output.split_multiallelic: true. The combined ALT copies must fit the site ploidy; if they do not (for example two hom-ALT alleles at a diploid site), the position is a no-call.

If no ALT reaches the threshold, the position is reference when at least consensus.threshold callers vote reference; otherwise it is a no-call.

Caller votes

Caller state Default vote
Caller has an ALT record supporting the allele ALT vote
Caller calls the reference Reference vote
Caller has no record Reference (consensus.absent_vote: "ref")
Caller has no record, absent_vote: "abstain" Reference only when the pileup has at least coverage.min_coverage reads
Caller record with missing genotype (./., 0/.) Abstain
Caller record with FILTER not PASS/. Abstain (consensus.filtered_vote: "abstain")
Caller record with FILTER not PASS/., filtered_vote: "ref" Reference

Hom-reference calls of any ploidy vote reference. A no-call is not a hom-reference vote, so a position whose remaining non-abstaining votes are below the threshold is not written.

Reference, no-call and ambiguous positions

  • Every covered position with a reference or variant consensus is written by default.
  • output.remove_invariant: true drops reference-only records at export.
  • Positions whose reference base is an IUPAC ambiguity code (N included), or whose caller allele contains one, are omitted because they cannot be represented with an unambiguous REF allele.
  • Consensus no-calls are omitted because there is no genotype to report.
  • Reference positions use the gVCF-style ALT="<*>" representation. See Output structure.

Deletions and BED chunks

After a variant record, the walk advances one base at a time, so caller records inside a deletion span are still voted on and variant calls under the deletion are kept. Reference records are not written over a called deletion's REF span, because a 0/0 call over deleted bases would contradict the deletion genotype; those positions are represented by the deletion record's POS..END. Deletion support is carried across coverage BED chunk boundaries so bases deleted in the previous chunk are not overwritten by reference records.

Caller failures and degraded mode

  • Transient failures (exit status 137 or 140) are retried up to resources.max_retries times.
  • Any other caller failure terminates the run by default.
  • calling.allow_caller_failure: true enables degraded mode: the failed caller is skipped with a warning, the surviving callers vote, and the run aborts if consensus.threshold exceeds the number of survivors.
  • Failed callers are recorded in CallersFailed in the consensus header.
  • A sample whose every caller fails is dropped. It is listed in dropped_samples.txt and in SamplesDropped in a merged header.

Performance

generate_consensus.py streams one cursor per caller BCF instead of materialising all records of a coverage span. Positions without a caller record take a fast path; BED intervals are grouped into spans that share one pileup iterator; and FORMAT/AD counts use bulk pileup accessors.

An optional Cython accelerator (bin/build_consensus_accel.py, built automatically in the Docker image) drives the pileup and writes records through htslib. Without it, an equivalent pure-pysam implementation is used and produces the same records.

Clone this wiki locally