zani ๐งฌ๐๏ธ๐คช¶
pronounced zany (/หzeษชni/)
High-Performance Average Nucleotide Identity (ANI) estimator using Zstandard compression distance.
๐ About¶
zani computes pairwise genomic distances using the Normalized Compression Distance (NCD) metric.
Originally written in Python, zani has been completely rewritten in Rust ๐ฆ for bare-metal multi-threaded performance ๐. It leverages the blazing-fast Zstandard (zstd) compression algorithm to estimate Average Nucleotide Identity (ANI) without the need for expensive sequence alignments or k-mer counting.
๐งฎ The Algorithm¶
At its core, zani treats reference genomes as compression dictionaries. For a given reference genome \(x\) and a query genome \(y\):
-
Dictionary Training ๐: A Zstd dictionary is trained on the reverseโcomplemented reference genome \(x\). The reverseโcomplement operation is mathematically defined as $\(\text{rc}(x)_i = \text{comp}(x_{L+1-i}),\)$ where \(\text{comp}(A)=T\), \(\text{comp}(T)=A\), \(\text{comp}(C)=G\), and \(\text{comp}(G)=C\). This guarantees that the algorithm always operates on the 5'โ3' strand regardless of input orientation.
-
Baseline Compression ๐: We compute \(C(x)\), the size of the reference genome compressed with its own dictionary.
-
Conditional Compression ๐๏ธ: The query genome \(y\) is compressed using the dictionary trained on \(x\). This yields \(C(y|x)\), representing the amount of novel information in \(y\) not found in \(x\).
๐ข The Math¶
zani calculates distance using the standard Normalized Compression Distance (NCD) formula:
To achieve maximum execution speed โก, zani approximates the joint compression size \(C(x,y)\) as:
Furthermore, to avoid the performance penalty of compressing the query genome twice to find its baseline \(C(y)\), zani rapidly estimates \(C(y)\) using the ratio of their uncompressed lengths (\(|x|\) and \(|y|\)):
๐งฑ The Chimeric Firewall¶
When concatenating multi-record draft genomes (e.g., thousands of small contigs) into a single sequence, zani automatically inserts a "Chimeric Firewall" ๐ก๏ธ of exactly 10 'N's (NNNNNNNNNN) between each contig boundary. Because Zstandard requires strictly contiguous exact-byte matches, this firewall mathematically guarantees that no LZ77 match can artificially bridge across two independent contigs, perfectly neutralizing artifactual ANI inflation without bloating the NCD baseline scores!
This mathematical approach, combined with zero-copy memoryviews, L1 cache optimization, and thread-safe C-contexts natively in Rust ๐ฆ, allows zani to stream thousands of genomes through concurrent worker threads, achieving massive I/O throughput and utilizing 100% of available CPU cores.
Installation¶
zani is distributed as native pre-compiled binaries for Windows, macOS, and Linux.
๐ป CLI Usage¶
zani now comes with a compiled Rust CLI for massive throughput:
โฏ zani --help
๐งฌ zani: Zstandard-based Average Nucleotide Identity (ANI) ๐๏ธ
Usage: zani <COMMAND>
Commands:
ava ๐ฅ Compute an all-vs-all pairwise distance matrix
search ๐ Compute distances of queries against a compiled target database
build ๐พ Pre-compile a database of genomes to disk
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
๐ฅ Running an All-vs-All Matrix¶
๐ Searching against a Database¶
๐พ Compiling a Target Database¶
๐ Python API Usage¶
The Python API has been completely rebuilt as a native PyO3 extension. It acts exactly like native Python objects, but drops the Global Interpreter Lock (GIL) to execute purely in Rust!
[!NOTE] Extensive Python API documentation is available! You can generate the HTML docs locally by running
just doc-api(outputs todocs/api).
import zani
# 1. Initialize an empty native database ๐๏ธ
db = zani.Database()
# 2. Add FASTA files to compile Zstandard dictionaries ๐๏ธ
db.add_fasta("genome_1.fasta", level=3, concat=True)
db.add_fasta("genome_2.fasta", level=3, concat=True)
# 3. Save/Load the compiled database to disk ๐พ (Optional)
db.save("my_genomes.zani")
# 4. Initialize the execution Engine ๐๏ธ
engine = zani.Engine(compression_level=3, batch_size=10_000)
# 5. Run the multithreaded matrix! ๐ (Releases the GIL)
engine.all_vs_all(db, output_filepath="results.tsv")
# Or search queries against a target database! ๐
engine.search(db, queries=db, output_filepath="results.tsv")
๐ญ Feedback¶
โ ๏ธ Issue Tracker¶
Found a bug ? Have an enhancement request ? Head over to the GitHub issue tracker if you need to report or ask something. If you are filing in on a bug, please include as much information as you can about the issue, and try to recreate the same bug in a simple, easily reproducible situation.