A faithful, memory-safe, SIMD-accelerated native-Rust reimplementation of spoa — the C++ partial order alignment (POA) library for consensus generation and multiple sequence alignment.
Partial order alignment builds a directed acyclic graph from a set of related sequences: each sequence is aligned to the graph built so far and merged into it, so shared subsequences collapse onto shared paths. The resulting DAG yields a consensus sequence and a multiple sequence alignment (MSA), and can be exported as GFA or Graphviz DOT.
spoars reproduces spoa v4.1.5's output bit-for-bit — the same dynamic-programming tie-breaks, the same consensus and MSA, the same GFA/DOT — verified against the C++ library through a differential oracle. Where a C++ idiom and a natural Rust one would diverge in observable behavior, the C++ behavior wins.
- Bit-exact with spoa across all nine alignment modes:
{Local, Global, Overlap}×{linear, affine, convex}gap penalties. - SIMD-accelerated, with a portable scalar fallback that produces identical results. Runtime dispatch to SSE4.1 / AVX2 on x86-64 and NEON on aarch64.
#![deny(unsafe_code)]everywhere except the isolated SIMD kernels module.- Consensus generation (with a minimum-coverage variant), MSA, and GFA/DOT export.
- Read-only accessors for inspecting the built graph, and an
AlignmentEnginetrait you can implement yourself. - Optional
serdefeature (off by default) for structural (de)serialization of a builtGraph.
use spoars::align::{AlignmentEngine, AlignmentType, Scoring, SimdEngine};
use spoars::graph::Graph;
// Match, mismatch, gap-open, gap-extend, and a second (convex) gap-open/extend pair.
let scoring = Scoring::new(5, -4, -8, -6, -10, -4).unwrap();
let mut engine = SimdEngine::new(AlignmentType::Global, scoring);
let reads: [&[u8]; 3] = [b"ACGTACGT", b"ACGTTCGT", b"ACGTACGT"];
let mut graph = Graph::new();
for read in reads {
let (alignment, _score) = engine.align(read, &graph);
graph.add_alignment_weight(&alignment, read, 1).unwrap();
}
assert_eq!(graph.generate_consensus(), "ACGTACGT");
let msa = graph.generate_msa(false); // one row per read
assert_eq!(msa.len(), 3);Python bindings (built with maturin, sources in spoars-py/) wrap the same engine:
pip install spoarsimport spoars
g = spoars.poa(["ACGTACGT", "ACGTTCGT", "ACGTACGT"])
g.consensus() # "ACGTACGT"
g.msa() # ['ACGTACGT', 'ACGTTCGT', 'ACGTACGT']
# Or build incrementally with an alignment type + scoring:
g = spoars.Poa(alignment_type="global", scoring=spoars.Scoring.default())
for read in ["ACGTACGT", "ACGTTCGT", "ACGTACGT"]:
g.add(read)
g.consensus(min_coverage=2)
consensus, coverage = g.consensus(with_coverage=True) # (str, list[int])
consensus, matrix = g.consensus_composition() # (str, list[list[int]])
g.gfa() # GFA v1
# Cache or transmit a graph: pickle, or JSON via to_json / from_json.
import pickle
restored = pickle.loads(pickle.dumps(g))
restored = spoars.Poa.from_json(g.to_json())
# Inspect the graph directly (node ids are plain ints):
g.rank_order() # node ids in topological order
g.node_coverage(3) # how many sequences pass through node 3
g.node_base(3) # 'T' (or None)
g.sequence_path(0) # node ids sequence 0 traverses
g.edges() # [(tail, head, weight), ...]See spoars-py/README.md for the full Python API.
Alignment goes through the AlignmentEngine trait. Two implementations ship:
SisdEngine— a portable scalar engine, correct on every target.SimdEngine— runtime-dispatched SSE4.1 / AVX2 (x86-64) or NEON (aarch64) with a scalar fallback, bit-identical toSisdEngine. It vectorizes the DP fill and reuses the scalar backtrack, so the accelerated path never changes the result — only the speed.
On one representative consensus workload (200 reads × ~1 kbp, convex gaps; --release build), SimdEngine ran roughly 4–5× faster than SisdEngine on the machines measured below. These are observed results for that specific setup, not a guarantee — the speedup varies with CPU, toolchain, and input shape (read count, length, gap mode), so benchmark your own workload before relying on a number.
| ISA | machine measured | speedup vs scalar |
|---|---|---|
| NEON | Apple M-series / AWS Graviton | ~4.9× |
| AVX2 | x86-64 | ~4.0× |
| SSE4.1 | x86-64 | ~4.0× |
x86 SIMD is memory-bound on the striped→row-major de-stripe rather than on the vectorized fill, which is why AVX2 and SSE4.1 land close together; a faithful (non-Farrar, non-int8) port keeps the row-wise recurrence that makes them comparable.
You can also implement AlignmentEngine yourself — for a different scoring model, a banded fill, or a test mock — and feed the result straight into Graph::add_alignment. See the trait's documentation for the alignment format and a worked example.
Beyond building and summarizing the graph, Graph exposes read-only accessors so downstream code can walk the DAG directly: nodes() / edges() and their node() / edge() id lookups, num_nodes() / num_edges(), encode() / decode() to convert between raw bytes and internal symbol codes, and rank_order() / sequence_starts() / consensus_nodes() for traversal. Node carries coverage(), successor(), and base() helpers.
Correctness is verified two ways:
- A C++ differential oracle (under
oracle/) links the pinned spoa submodule (built without-marchflags, forcing its scalar path) and is compared againstspoarson generated inputs via property-based tests. - The scalar
SisdEnginethen serves as an in-process oracle for the SIMD kernels: everySimdEngineresult is checked bit-for-bit againstSisdEngineacross all nine modes and both the int16 and int32 lane widths.
See CONTRIBUTING.md for build/test setup, the git hooks, and the bit-exact faithfulness contract.
MIT — see LICENSE. Third-party attribution (spoa) is in THIRD-PARTY.md.