K-mer Analysis
K-mers are contiguous subsequences of length k extracted from a longer sequence. They are foundational to many bioinformatics algorithms including genome assembly, sequence comparison, metagenomic classification, and error correction. BioLang provides optimized k-mer functions that use 2-bit encoding for k ≤ 32, enabling memory-efficient counting of billions of k-mers.
kmer_encode / kmer_decode
Convert between k-mer strings and their integer encodings. Each base is encoded as 2 bits (A=00, C=01, G=10, T=11), so a k-mer of length k requires 2k bits:
# Encode a k-mer to its integer representation
# kmer_encode(sequence, k)
let code = kmer_encode(dna"ATCG", 4) # => Kmer (encodes as 27)
code = kmer_encode(dna"AAAA", 4) # => 0
code = kmer_encode(dna"TTTT", 4) # => 255
# Decode back to a string
let seq = kmer_decode(kmer_encode(dna"ATCG", 4)) # => "ATCG"
seq = kmer_decode(kmer_encode(dna"AAAA", 4)) # => "AAAA"
kmer_count
Count all k-mer occurrences in a sequence, list, table, or stream. Returns a Table
with kmer and count columns, sorted by count descending.
Accepts DNA, RNA, strings, lists of sequences, records with a seq field,
and FASTQ streams.
let seq = dna"ATCGATCGATCGATCG"
# Count all 4-mers — returns a Table sorted by count (descending)
let counts = kmer_count(seq, 4)
print(counts) # Table: kmer | count
# Stream mode — constant memory for large FASTQ files
read_fastq("data/reads.fastq")
|> kmer_count(21)
|> head(10)
|> print()
Memory Options for Large Datasets
For whole-genome or deep-sequencing data, k-mer counting can generate millions of unique entries. BioLang provides three strategies to manage memory:
| kmer_count(seq, 21) | Default — in-memory up to ~2M unique k-mers, then auto-spills to a temporary SQLite database on disk. No code changes needed. |
| kmer_count(seq, 21, 100) | Top-N mode — only keeps the top N most frequent k-mers. Uses bounded memory with periodic pruning. Best when you only need the most common k-mers. |
| fastq(...) |> kmer_count(21) | Streaming — processes reads one at a time without loading all reads into memory. Combine with head(n) to limit output. |
The result is always sorted by count descending — no need for sort_by after kmer_count.
kmer_spectrum
The k-mer spectrum (frequency-of-frequencies) shows how many k-mers appear exactly
1 time, 2 times, etc. This is useful for genome size estimation and error rate
analysis. Pass a counts map from kmer_count:
let seq = dna"ATCGATCGATCGATCGATCG"
# Compute k-mer frequency spectrum
let spectrum = kmer_spectrum(kmer_count(seq, 4))
# => Table with frequency and count columns
spectrum |> print()
minimizers
Minimizers are a subsampling scheme that selects the lexicographically smallest k-mer within each window of size w. They are used in minimap2, Kraken, and many other tools for efficient sequence comparison and indexing:
let seq = dna"ATCGATCGATCGATCGATCGATCG"
# Extract minimizers: minimizers(seq, k, window)
let mins = minimizers(seq, 7, 11)
# Returns a list of { kmer, pos } records
mins |> map(|m| print(m.pos, m.kmer))
# Minimizer density (minimizers per base)
let density = float(len(mins)) / float(seq_len(seq))
print("Minimizer density:", density)
kmer_encode / kmer_decode
Convert between k-mer strings and their compact integer encodings, useful for efficient storage and comparison:
# Encode a k-mer to its 2-bit integer representation
let code = kmer_encode(dna"ATCG", 4)
print(code) # integer encoding
# Decode back to string
let seq = kmer_decode(code)
print(seq)
Practical Example: Metagenomic Classification
K-mer analysis enables fast taxonomic classification without alignment:
# Top 21-mers across all reads — streaming, bounded memory
read_fastq("data/reads.fastq")
|> kmer_count(21, 50)
|> print()
# Or full k-mer profile (auto-spills to disk for large datasets)
read_fastq("data/reads.fastq")
|> kmer_count(21)
|> head(20)
|> bar_chart("Top 21-mers")
kmer_index
Build an index from every k-mer in a sequence to the sorted list of all positions where
it starts. Returns Map<Str, List<Int>>. This is particularly
useful for repeat detection, seed-and-extend alignment, and any workflow that needs to
know where a k-mer occurs rather than just how often.
Signature: kmer_index(seq, k) → Map<Str, List<Int>>
let seq = dna"ATCGATCGATCG"
let idx = kmer_index(seq, 3)
# idx is {ATC: [0, 3, 6], TCG: [1, 4, 7], CGA: [2, 5], GAT: [3], ...}
# Look up positions of a specific k-mer
println(idx["ATC"]) # => [0, 3, 6]
println(idx["TCG"]) # => [1, 4, 7]
# Count occurrences
println(len(idx["ATC"])) # => 3
# Find k-mers appearing more than once (repeat elements)
let repeats = keys(idx) |> filter(|kmer| len(idx[kmer]) > 1)
println(len(repeats), "k-mers appear more than once")
# Build a k-mer index for a FASTA file and detect shared k-mers
let genome_record = fasta("genome.fa") |> first()
let genome = genome_record.seq
let k = 21
let genome_idx = kmer_index(genome, k)
println("Unique 21-mers:", len(genome_idx))
# Find positions of a specific 21-mer
let target = "ATCGATCGATCGATCGATCGA"
let positions = genome_idx[target] ?? []
println("Target k-mer at positions:", positions)
See also:
Advanced Sequence Analysis
for find_pattern, gc_skew, sliding window operations, and
other low-level sequence primitives that complement k-mer indexing.