Kestrel Genotyping¶
Kestrel is the primary genotyping engine in VNtyper. It performs mapping-free, k-mer-based variant calling against a MUC1 VNTR reference sequence. The downstream postprocessing pipeline filters, scores, and annotates candidate variants.
Why Mapping-Free Genotyping?¶
The VNTR alignment problem
Traditional read alignment struggles with VNTRs because tandem repeats create ambiguous alignments. A read carrying a frameshift insertion within one repeat unit may align equally well across multiple positions in the array. Mapping-free approaches bypass this by analyzing k-mer frequency spectra directly, avoiding reference alignment bias.
Kestrel scans ordered k-mer frequency spectra from input reads, detecting active regions where frequencies drop relative to flanking sequence. Within each active region, it reconstructs local haplotypes by iteratively extending from high-frequency anchor k-mers using a modified Smith-Waterman alignment. Mismatches between reconstructed haplotypes and the reference identify insertions and deletions.
Kestrel Parameters¶
| Parameter | Default | Description |
|---|---|---|
kmer_sizes | [20] | K-mer length for graph construction |
java_memory | 12g | JVM heap allocation, applied to both steps |
max_align_states | 40 | Maximum alignment states during path enumeration |
max_hap_states | 40 | Maximum haplotype states for genotype resolution |
additional_settings | "" | Extra command-line flags appended to calling step |
split_counting | true | Count k-mers in a separate KAnalyze step |
keep_ikc | false | Retain attempt directory and k-mer count after run |
java_opts_count | "" | Extra JVM options for counting step |
java_opts_call | -XX:+UseSerialGC | Extra JVM options for calling step |
These parameters are configured in kestrel_config.json under kestrel_settings.
java_memory is load-bearing
The 12g setting is calibrated for performance. Lowering it to 4g increases runtimes and CPU time several-fold due to G1 garbage collection thrashing. The counting step peaks near 7.7 GB at high thread counts. Peak RSS on a small run does not justify reducing this allocation.
Kestrel Execution¶
VNtyper runs Kestrel by default as two commands per k-mer size: a threaded KAnalyze counting step, followed by Kestrel reading the pre-built counts.
Why counting is a separate step¶
Kestrel 1.0.1 builds a KAnalyze CountModule without configuring thread counts. KestrelRunnerBase.getCountModule() sets the k-mer size, temporary directory, post-count filter, and free-segment flag, but leaves setKmerThreadCount, setSplitThreadCount, and setThreads at their single-threaded defaults. Counting represents most of Kestrel's runtime on unmapped-rich inputs.
Running the bundled kanalyze.jar as a distinct command enables multi-core execution. Kestrel natively accepts a pre-built indexed k-mer count (IKC) file: IkcCountMap.preModuleRun adopts the supplied ikc file, sets rmLastTemp = false (ensuring Kestrel never deletes the count file), and bypasses internal counting.
# Step 1: count, threaded
java -Xmx12g -jar kanalyze.jar count -k 20 \
-c kmercount:5 --minsize 15 -m ikc \
-d <kmer> -l <split> -t <sort> \
--temploc <out>/kmer_20 -o <out>/kmer_20/kestrel_kmers.ikc \
R1.fastq.gz R2.fastq.gz single.fastq.gz
# Step 2: call, reading counts
java -Xmx12g -XX:+UseSerialGC -jar kestrel.jar -k 20 \
--maxalignstates 40 --maxhapstates 40 \
-r <muc1_reference.fa> -o output.vcf \
-s<sample_name> -f ikc <out>/kmer_20/kestrel_kmers.ikc \
--hapfmt sam -p output.sam
The --threads allocation is partitioned across KAnalyze's three concurrent stages: -d (k-mer extraction), -l (split), and -t (sorting). These are distinct pipeline stages rather than redundant worker flags; assigning --threads N to all three would launch approximately 2.5N workers. The k-mer stage receives the largest allocation as the primary bottleneck. Each stage is floored at one worker.
JVM options differ between steps: counting is parallel and uses the default G1 collector, whereas calling is single-threaded and uses -XX:+UseSerialGC to prevent G1 from launching unnecessary GC threads. Parallel GC (-XX:+UseParallelGC) degrades performance on both steps.
Kestrel outputs a VCF file of detected variants and a SAM file containing one alignment record per resolved haplotype. The SAM file is converted to indexed output.bam for IGV visualization. These records represent resolved haplotype sequences rather than original reads. The optional XD tag records minimum k-mer depth for the haplotype; VNtyper records it separately and does not use it to weight votes.
Output layout and logs¶
Each k-mer attempt writes to <output>/kmer_<size>/. Isolating runs in dedicated directories prevents collisions between concurrent attempts and enables clean recursive cleanup. The counting step writes to <output>/kanalyze_count_kmer_<size>.log, separated from the Kestrel log to preserve error diagnostics.
The attempt directory is removed upon exit across all outcomes (failure and success). Setting keep_ikc: true preserves the directory for debugging. If the directory already exists prior to running, the pipeline refuses to execute to prevent deleting external data.
The chosen execution path is recorded in pipeline_summary.json under kestrel_counting_mode as split or internal.
Disabling split counting¶
Setting split_counting: false restores the single-command workflow. This setting is an operator kill switch, not an automated fallback: a failed counting step raises an exception rather than silently retrying internally.
Allowlist for additional_settings¶
When using pre-built IKC counts, additional_settings permits only options that cannot alter k-mer counting. Any disallowed option raises a configuration error prior to execution.
When counting is split, count-modifying parameters must be applied to both commands. Kestrel validates only k-mer size when opening an IKC file; counts generated with differing minimum counts or minimizer sizes are accepted silently, altering depth scores and genotype calls.
Disallowed flags include counter options (--mincount, --minsize, --minmask, --charset, --seqfilter, --quality, -k/--ksize, --temploc, --free/--nofree, --lib/--liburl), count-map selectors (--memcount/--nomemcount), IKC lifecycle controls (--rmikc/--normikc), and options configured directly by VNtyper.
Count-affecting settings
To configure count-affecting parameters, set kestrel_settings.split_counting: false to run stock single-command Kestrel.
Postprocessing Pipeline¶
Following VCF generation, VNtyper applies a nine-step postprocessing pipeline:
flowchart TD
S1[Step 1: VCF Parsing & INDEL Filtering] --> S2[Step 2: Split Insertions / Deletions]
S2 --> S3[Step 3: Depth Splitting & Frame Score]
S3 --> S4[Step 4: Confidence Assignment]
S4 --> S5[Step 5: ALT-Based Filtering]
S5 --> S6[Step 6: Motif Correction & Annotation]
S6 --> S7[Step 7: Flagging]
S7 --> S8[Step 8: Final Filtering & Variant Selection]
S8 --> S9[Step 9: Output Generation] Step 1: VCF Parsing and INDEL Filtering¶
Filters the raw Kestrel VCF to retain only INDELs, discarding SNVs. Standardizes header format to VCFv4.2. If bcftools is available, compresses and indexes the VCF (output_indel.vcf.gz).
Step 2: Split Insertions and Deletions¶
Splits INDELs into output_insertion.vcf and output_deletion.vcf. Loads each into pandas DataFrames, merges with the MUC1 reference motif table, annotates variant type, and recombines the records.
Step 3: Depth Splitting and Frame Score Calculation¶
Splits the Kestrel Sample field (DEL:AltDepth:ActiveRegionDepth) into alternate-allele k-mer-path depth and total active-region k-mer depth. Calculates frame score:
Frame Score = (len(ALT) - len(REF)) / 3
Sets is_frameshift = True when (len(ALT) - len(REF)) % 3 != 0.
Step 4: Confidence Assignment¶
Computes depth score (Alt_Depth / Active_Region_Depth) and assigns confidence labels (High_Precision*, High_Precision, Low_Precision, Negative) using calibrated thresholds. Calculates haplo_count (the frequency of identical POS, REF, ALT calls across reconstructed haplotypes).
Step 5: ALT-Based Filtering¶
Filters out known false-positive sequence motifs (such as CCGCC, CGGCG, CGGCC) and incompatible motif pairings.
Step 6: Motif Correction and Annotation¶
Annotates variants with MUC1 repeat unit identities (~60 bp motifs such as X, Y, Z, 1, 2, 3, Q).
MUC1 VNTR motif structure
The reference FASTA encodes repeat unit junctions as paired entries (MotifLeft-MotifRight). Variants at position < 60 fall within the first repeat unit and receive the right-hand motif name; variants at position >= 60 fall within the second unit and receive the left-hand motif name.
Filters out variants within invariant motifs (Q, 8, 9, 7, 6p, 6, V, J, I, G, E, A) that represent sequencing artifacts.
Step 7: Flagging¶
Applies configurable boolean rule trees to tag false positives and artifacts. Evaluated before variant selection so unflagged candidates take precedence.
Step 8: Final Filtering and Variant Selection¶
Evaluates six boolean gate columns:
is_frameshift: Must cause a reading-frame shift.is_valid_frameshift: Must follow insertion (3n+1) or deletion (3n+2) pathogenic signatures.depth_confidence_pass: Confidence must not be Negative.alt_filter_pass: Must pass ALT sequence filters.motif_filter_pass: Must pass motif position filters.flag_filter_pass: Must not carry any flag declared inartifact_flags.
Retains only rows where all gates evaluate to True. Selects the single best variant using deterministic priority ranking:
- Highest confidence tier (High_Precision* > High_Precision > Low_Precision)
- Unflagged preferred over flagged
- Highest depth score
- Highest haplo_count
- Lowest genomic position (deterministic tie-breaker)
Step 9: Output Generation¶
Writes final calls to kestrel_result.tsv with run metadata. Generates output.bed for IGV visualization. Retains unfiltered candidate rows in kestrel_pre_result.tsv for provenance.
The Below-Reporting-Floor Note¶
A candidate variant that passes all structural gates but fails the depth score threshold is a valid pathogenic-frame variant below the calling floor. Under Issue #266, such variants trigger an informational ## banner comment in kestrel_result.tsv:
## VNtyper Kestrel result
## VNtyper Version: 2.0.23
## Analysis date: 2026-08-26 21:59:44
## Reference file: reference/All_Pairwise_and_Self_Merged_MUC1_motifs_filtered.fa
## Subthreshold candidate: 4 candidate variants in the pathogenic frame identified below the
reporting floor (best Depth_Score 0.0040072, floor 0.00469); filtered out and NOT a call.
Depth_Score is a ratio against an active-region depth summed over the repeat array, so the
same alternate k-mer-path depth scores lower on a longer allele.
The banner is strictly a comment line. summary.parse_tsv routes # lines into comments, leaving the 10-column result table unchanged. Downstream tools reading table rows do not register a call.
Qualifying criteria¶
A candidate triggers the note only when every structural gate is True and depth_confidence_pass is explicitly False:
| Gate | Rationale for disqualification |
|---|---|
is_frameshift, is_valid_frameshift | Non-pathogenic reading frames are not disease candidates. |
alt_filter_pass | Excluded ALT patterns represent known technical noise. |
motif_filter_pass | Non-target motif regions are excluded from calling. |
flag_filter_pass | Artifact flags represent deliberate exclusions (Issue #174); strong artifact signals must not be mislabeled as subthreshold true signal. |
Missing, blank, or non-boolean verdicts disqualify a row.
Occurrence profile¶
The note appears exclusively on samples where no variant was called. On the 400-sample benchmark cohort (200 carriers, 200 controls):
| Sample Group | Total | Samples with subthreshold note |
|---|---|---|
| False negatives | 22 | 22 |
| True negatives | 200 | 1 |
| Called samples | 178 | 0 |
Positive calls suppress the note to avoid displaying redundant sub-optimal descriptions of the called variant.
Operational boundaries¶
The reporting floor remains unchanged at 0.00469. A subthreshold candidate is not a call. The report marks the screening state as negative_subthreshold, categorized as a non-finding under algorithm_logic.kestrel.non_finding_results in report_config.json. Cohort analysis classifies the sample as Negative.
Depth score is a ratio against active-region depth summed over the repeat array. A mutant unit among N repeat copies scores approximately 1/N regardless of sequencing depth; identical alternate k-mer depths yield lower scores in longer repeat arrays.
To disable the note, set subthreshold_note.enabled: false in kestrel_config.json.
Reference¶
Saei H. et al., iScience 26, 107171 (2023).