Somatic vs Germline Variant Calling: Tumor-Normal Pairs with Mutect2
Why somatic calling needs a different statistical model than germline, plus a real Mutect2 tumor-normal run from BAM to a filtered somatic VCF.
Point a germline variant caller at a tumor sample and it will confidently hand you thousands of variants at roughly 0.5 and 1.0 allele fraction, then quietly miss the mutation that actually matters. Cancer breaks the core assumption every germline caller is built on, and that broken assumption is the whole reason a separate tool like Mutect2 exists. This post explains why somatic calling needs a fundamentally different statistical model, then walks a real tumor-normal pair from analysis-ready BAMs to a filtered somatic VCF.
Why Germline Models Fail on Tumors
A germline caller like HaplotypeCaller assumes you are looking at a diploid genome with a small, fixed set of possible genotypes. Every real variant should appear at an allele fraction near 0.5 (heterozygous) or 1.0 (homozygous), because the person inherited it in every cell. The caller uses that expectation as a prior: it asks "which of these clean diploid genotypes best explains the reads?" and rejects anything that does not fit.
Somatic mutations violate that prior on every axis. A mutation that arose in one cell and spread through part of the tumor might sit at an allele fraction of 0.12, or 0.04, or lower. Tumor purity dilutes it further, because your biopsy is a mix of cancer and normal stroma. Subclonal architecture means different mutations exist at different fractions in the same sample. To a diploid model, a true somatic variant at 0.06 allele fraction is statistically indistinguishable from a sequencing error, so it gets filtered out. Somatic calling therefore abandons the fixed-genotype model entirely and asks a different question: is there evidence of an allele here that is not explained by sequencing error or by the matched normal?
The Tumor-Normal Design
The single most powerful idea in somatic calling is sequencing a matched normal from the same patient, usually blood or adjacent healthy tissue. The normal gives you that patient's private germline background. Any variant present in both the tumor and the normal is inherited, not acquired, so you subtract it out. What remains is the somatic signal.
| Signal source | Present in tumor | Present in normal | Called somatic? |
|---|---|---|---|
| Germline SNP/indel | Yes | Yes | No, subtracted |
| True somatic mutation | Yes (often low AF) | No | Yes |
| Recurrent artifact | Yes | Sometimes | Filtered by PoN |
| Sequencing error | Random | Random | Filtered statistically |
Without a matched normal you are in "tumor-only" mode, where you lean much harder on population databases and a panel of normals to guess which variants are germline. It works, but it produces noisier calls, so a paired design is always preferred when tissue and budget allow. If the upstream alignment and preprocessing feel unfamiliar, the germline walkthrough in Variant Calling from FASTQ to VCF with GATK4 covers the BWA-MEM, MarkDuplicates, and BQSR steps that produce the analysis-ready BAMs this post starts from.
Prerequisites and Inputs
You need gatk4 (which bundles Mutect2), samtools for BAM inspection, a reference genome with its .fai and .dict indexes, and both tumor and normal BAMs that have already been through duplicate marking and base quality recalibration. If you have not installed the toolkit yet, Install GATK4 with Conda gets you there in a few minutes.
Confirm the read groups first, because Mutect2 identifies which BAM is the tumor and which is the normal by the sample name in the SM tag, not by filename:
samtools view -H tumor.bam | grep '^@RG'
samtools view -H normal.bam | grep '^@RG'You also want two public resources from the GATK bundle: a germline resource such as gnomAD (allele frequencies used for contamination estimation) and, ideally, a panel of normals.
The normal sample name you pass to -normal must match the SM value inside the normal BAM exactly, not the file name. If Mutect2 errors with "sample not found" this is almost always why. Copy the string straight out of the @RG line rather than typing it.
Building a Panel of Normals
A panel of normals (PoN) is Mutect2's defense against recurrent technical artifacts, the kind of false positives that show up in many unrelated samples because of a sequencing chemistry quirk, a mapping-prone region, or an oxidation artifact during library prep. If a site keeps appearing across many normal samples, it is almost certainly not a real cancer mutation in your patient.
You build a PoN in three steps: run Mutect2 on each normal in tumor-only mode, aggregate them into a GenomicsDB, then collapse that into a single VCF.
# 1. Call each normal in tumor-only mode (repeat for every normal sample)
gatk Mutect2 -R reference.fasta -I normal1.bam \
--max-mnp-distance 0 -O normal1.pon.vcf.gz
# 2. Aggregate the per-normal VCFs into a GenomicsDB workspace
gatk GenomicsDBImport -R reference.fasta \
--genomicsdb-workspace-path pon_db \
-V normal1.pon.vcf.gz -V normal2.pon.vcf.gz -V normal3.pon.vcf.gz \
-L intervals.list
# 3. Combine into the final PoN
gatk CreateSomaticPanelOfNormals \
-R reference.fasta \
--germline-resource af-only-gnomad.vcf.gz \
-V gendb://pon_db -O pon.vcf.gzThe --max-mnp-distance 0 flag is required in step 1 so the per-normal VCFs merge cleanly. Use at least 20-40 normals sequenced on the same platform and library prep as your tumors, since the whole point is to capture your assay's artifacts. A PoN from a different center will miss the artifacts that matter to you.
Running Mutect2 on the Pair
With the PoN and germline resource ready, the core call is a single command. Mutect2 uses a local haplotype-assembly approach much like HaplotypeCaller, but with a somatic likelihood model that estimates allele fractions continuously instead of forcing them onto diploid genotypes:
gatk Mutect2 \
-R reference.fasta \
-I tumor.bam \
-I normal.bam \
-normal normal_sample_name \
--germline-resource af-only-gnomad.vcf.gz \
--panel-of-normals pon.vcf.gz \
--f1r2-tar-gz f1r2.tar.gz \
-O unfiltered.vcf.gzThe --f1r2-tar-gz output is easy to skip but worth keeping: it collects the read-orientation counts needed to model FFPE deamination and oxo-G artifacts, sequencing errors that show up preferentially on one DNA strand. You feed it into LearnReadOrientationModel before filtering:
gatk LearnReadOrientationModel -I f1r2.tar.gz -O read-orientation-model.tar.gzNote that Mutect2 does not decide what is a real mutation at this stage. It emits every candidate with rich annotations and leaves the accept/reject decision to a separate filtering step. That separation is deliberate, and it is where most of the accuracy comes from.
Estimating Cross-Sample Contamination
Contamination is a quiet killer in somatic calling. If even a few percent of your tumor reads actually come from another individual, those foreign germline variants look exactly like low-allele-fraction somatic mutations and flood your call set with false positives. Mutect2 handles this by estimating how contaminated the sample is and passing that number to the filter.
The estimate uses common germline SNPs: at sites where the patient should be homozygous, any reads carrying the alternate allele are evidence of contamination. GetPileupSummaries tabulates allele counts at known biallelic sites, and CalculateContamination turns them into a fraction:
gatk GetPileupSummaries -I tumor.bam \
-V common_biallelic.vcf.gz -L common_biallelic.vcf.gz \
-O tumor_pileups.table
gatk GetPileupSummaries -I normal.bam \
-V common_biallelic.vcf.gz -L common_biallelic.vcf.gz \
-O normal_pileups.table
gatk CalculateContamination \
-I tumor_pileups.table \
-matched normal_pileups.table \
-O contamination.table \
--tumor-segmentation segments.tableSupplying the matched normal with -matched sharpens the estimate by distinguishing genuine contamination from the patient's own heterozygous sites. A contamination fraction above roughly 0.05 is a red flag worth investigating before you trust any downstream calls.
Filtering to a Final Somatic VCF
FilterMutectCalls is where everything converges. It reads the unfiltered VCF plus the contamination table, the read-orientation model, and the tumor segmentation, then applies a single statistical model that sets a probability threshold to maximize the expected F-score across all filters at once:
gatk FilterMutectCalls \
-R reference.fasta \
-V unfiltered.vcf.gz \
--contamination-table contamination.table \
--tumor-segmentation segments.table \
--ob-priors read-orientation-model.tar.gz \
-O filtered.vcf.gzThis does not delete anything. It writes PASS in the FILTER column for calls that survive and a specific reason for those that do not, such as weak_evidence, germline, contamination, panel_of_normals, or orientation. To extract your confident somatic set, keep only the passing records:
bcftools view -f PASS filtered.vcf.gz -O z -o somatic.pass.vcf.gzRead the reject reasons before discarding them, because they diagnose your experiment: a flood of orientation flags points at FFPE damage, many contamination flags confirm a sample-mixing problem, and heavy panel_of_normals filtering suggests your PoN is doing real work. If the VCF fields themselves are unfamiliar, The VCF File Format Explained breaks down what each column and FILTER tag means.
Practical Takeaways
- Germline callers assume allele fractions of
0.5or1.0; somatic mutations sit anywhere from0.5down to a few percent, which is why Mutect2 uses a continuous allele-fraction model instead of fixed diploid genotypes. - A matched normal subtracts the patient's private germline background, so always prefer tumor-normal over tumor-only when tissue allows.
- Build your panel of normals from samples run on your platform and library prep; it exists to catch your assay's recurrent artifacts.
- Keep the
--f1r2-tar-gzoutput and runLearnReadOrientationModel, or FFPE and oxo-G artifacts will slip through as false positives. - Always estimate contamination with
GetPileupSummariesplusCalculateContamination; anything above0.05warrants investigation before you trust the calls. - Mutect2 emits candidates and
FilterMutectCallsjudges them; read the reject reasons, because they tell you what went wrong in the lab.
The mental shift from germline to somatic calling is less about new commands and more about a new statistical worldview: you are no longer fitting reads to a tidy diploid genome but hunting faint, uneven signals against a noisy background you have to model explicitly. Once that clicks, the Mutect2 pipeline reads as a series of noise-suppression steps rather than a black box. To go deeper on how variants become biological meaning, Variant Annotation with VEP, ANNOVAR, and SnpEff picks up where a filtered somatic VCF leaves off.