poa-consensus 0.1.1

Banded Partial Order Alignment (POA) consensus for short tandem repeat and amplicon reads
Documentation

poa-consensus

CI Crates.io docs.rs License: MIT Rust

A pure-Rust banded Partial Order Alignment (POA) library for building consensus sequences from a set of reads.

POA aligns reads into a directed acyclic graph (DAG) using affine-gap dynamic programming and extracts consensus by following the heaviest (most-supported) path. Insertions and deletions create branches resolved by read support.

Target use cases: short tandem repeat (STR) loci, amplicon consensus, structural variants, per-locus nanopore or HiFi read sets (50 bp to ~20 kb with banded DP). Can be paired with bedpull for building a consensus of extracted sequences.

The nearest alternative on crates.io is poasta (Broad Institute, pure Rust, gap-affine A* alignment), which excels at larger graphs such as bacterial genes and HLA loci. For STR reads where graphs stay small and throughput across many loci matters, banded DP is faster. Both crates are pure Rust with no C dependencies.

Usage

[dependencies]
poa-consensus = "0.1"

Functional API

use poa_consensus::{consensus, consensus_multi, consensus_adaptive, PoaConfig};

let reads: Vec<&[u8]> = vec![
    b"CATCATCAT",
    b"CATCATCAT",
    b"CATCGTCAT",
    b"CATCATCAT",
];

// Single-allele consensus; seed_idx seeds the graph with reads[seed_idx].
let result = consensus(&reads, 0, &PoaConfig::default())?;
println!("{}", String::from_utf8_lossy(&result.sequence));

// Multi-allele: returns one Consensus per detected allele.
let alleles = consensus_multi(&reads, 0, &PoaConfig::default())?;

// Adaptive two-pass: inspects graph statistics after pass 1 and automatically
// chooses multi-allele split, noise tightening, or semi-global switch for pass 2.
// Always returns a Vec<Consensus> (one element for single-allele outcomes).
let alleles = consensus_adaptive(&reads, 0, &PoaConfig::default())?;

Stateful API

use poa_consensus::{PoaGraph, PoaConfig};

let reads: &[&[u8]] = &[b"CATCATCAT", b"CATCATCAT", b"CATCGTCAT"];

let mut graph = PoaGraph::new(reads[0], PoaConfig::default())?;
for read in &reads[1..] {
    graph.add_read(read)?;
}
let consensus = graph.consensus()?;
let stats     = graph.stats();
println!("bubbles: {}", stats.bubble_count);

The stateful API lets you inspect graph state between reads and reuse pre-allocated buffers across calls, which is important for high-throughput per-locus pipelines.

Seed selection

use poa_consensus::{select_seed, SeedSelection};

// Automatically find the shortest fully-spanning read using terminal k-mer anchors.
// Falls back to longest read when no cluster structure is detected.
// Returns Err(NoSpanningReads) when reads split into two non-overlapping groups.
let seed_idx = select_seed(&reads, &SeedSelection::Auto)?;
let result = consensus(&reads, seed_idx, &PoaConfig::default())?;

Orientation utilities

use poa_consensus::{auto_orient, reverse_complement};

// Orient all reads to match the strand of reads[seed_idx] before POA.
let oriented = auto_orient(&reads, seed_idx);

Diagnostics

use poa_consensus::{diagnose, DiagnoseConfig};

let result = consensus(&reads, 0, &PoaConfig::default())?;
let warnings = diagnose(&result, &DiagnoseConfig::default());

if !warnings.is_clean() {
    for (is_warning, msg) in warnings.messages("consensus") {
        let level = if is_warning { "warning" } else { "note" };
        eprintln!("{level}: {msg}");
    }
}

diagnose checks four independent signals: read depth, coverage gaps, near-zero interior support, and structural competing alleles. All signals are also available as structured fields on ConsensusWarnings for programmatic handling.

CLI

cargo install poa-consensus --features cli
poa-consensus reads.fa                    # FASTA or FASTQ, auto-detected
poa-consensus reads.fa --multi            # multi-allele mode
poa-consensus reads.fa --adaptive-band    # recommended for reads > 1 kb
poa-consensus reads.fa --quiet            # suppress warnings; errors always printed

Feature flags

Flag Adds
cli Binary target; pulls in clap + noodles
plot SVG visualisation helpers via kuva

Default build: library only, zero external dependencies.

AI disclosure

This library was developed with AI assistance (Claude). Architecture decisions, testing, validation, and algorithm designs, are the author's own. AI tooling served as an accelerator over existing skill. The library originated from prototyping a POA implementation inside bladerunner, a nanopore STR detector and genotyper, and was spun out as a standalone crate when existing POA crates did not meet the throughput and API requirements of that use case.

License

MIT - James Ferguson 2026