Skip to content

zani ๐Ÿงฌ๐Ÿ—œ๏ธ๐Ÿคช

pronounced zany (/หˆzeษชni/)

CI Docs Benchmarks Coverage

Crates.io PyPI Python Versions

Release License DOI

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\):

  1. 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.

  2. Baseline Compression ๐Ÿ“: We compute \(C(x)\), the size of the reference genome compressed with its own dictionary.

  3. 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:

\[ NCD(x,y) = \frac{C(x,y) - \min(C(x), C(y))}{\max(C(x), C(y))} \]

To achieve maximum execution speed โšก, zani approximates the joint compression size \(C(x,y)\) as:

\[ C(x,y) \approx C(x) + C(y|x) \]

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|\)):

\[ C(y) \approx C(x) \times \frac{|y|}{|x|} \]

๐Ÿงฑ 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.

pip install zani

๐Ÿ’ป 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

zani ava genome1.fasta genome2.fasta --output zani_matrix.tsv --level 3

๐Ÿ” Searching against a Database

zani search query1.fasta query2.fasta --db my_database.zani --output results.tsv

๐Ÿ’พ Compiling a Target Database

zani build genome1.fasta genome2.fasta --db my_database.zani

๐Ÿ 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 to docs/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.