Feature statistics Accumulator for Base-pair-level Analysis
faba extracts per-cell genomic features directly from alignment (BAM) files:
RNA modifications (DART-seq m6A, A-to-I editing), alternative polyadenylation
(APA), gene counts, read depth, and SNP genotypes.
Run in a terminal, run, qc, pileup, metagene, pwm and cluster-view open full-screen
views; --batch-process skips them for scripts and clusters.
Each view runs as a detachable session: Ctrl+Alt+D leaves it running (as does a dropped
connection), and starting the command again with nothing named, or faba attach, returns
to it; Ctrl+Alt+S switches to another session. faba sessions lists them, faba kill
stops one as its quit key does (a write under way finishes first) and faba rename
labels one. --no-detach runs a view in the terminal only.
faba run: pick the BAMs (signal and control), steps and flags, preview the command, and run it.
faba qc: set site thresholds, see what each one keeps, and write a filtered copy.
faba pileup: browse a gene's modification sites, one track per batch.
faba pwm: the sequence logo around the called sites.
These must be installed before running cargo install — Cargo only builds
Rust crates and cannot pull in a C toolchain or system libraries on its own. If
they are missing, the build fails midway in the vendored-htslib build script.
-
Rust (stable, edition 2021+) — install via rustup.
-
A C/C++ toolchain and the system libraries that
rust-htslibbuilds against (it vendors and compiles htslib, which needs the compression dev headers, and runsbindgen, which needs libclang):# Debian / Ubuntu sudo apt-get install build-essential clang libclang-dev \ zlib1g-dev libbz2-dev liblzma-dev pkg-config # macOS (Homebrew) brew install llvm xz bzip2 zlib
cargo install fabaOptional HDF5 (.h5 / .h5ad) support:
cargo install faba --features hdf5cargo install --git https://github.com/causalpathlab/faba.gitPin to a specific tag or branch if you need a reproducible build:
cargo install --git https://github.com/causalpathlab/faba.git --tag v0.14.0
cargo install --git https://github.com/causalpathlab/faba.git --branch maingit clone https://github.com/causalpathlab/faba.git
cd faba
cargo build --release
cargo install --path .| Feature | Enables | Extra requirement |
|---|---|---|
mcp (default) |
faba mcp, the commands as tools for an agent |
none |
hdf5 |
HDF5 backend + .h5/.h5ad readers |
libhdf5 on the build host |
There is no cuda / metal feature: nothing on the BAM-to-matrix path touches a
GPU.
Add them with --features:
cargo install --git https://github.com/causalpathlab/legume-rs.git faba --features hdf5faba --helpAlmost all install failures come from the hts-sys build step (it compiles a
vendored copy of htslib and generates bindings with bindgen). The fixes:
-
thread 'main' panicked ... Unable to find libclang—bindgencan't locate libclang. Installlibclang-dev(Debian/Ubuntu) orbrew install llvm(macOS), then point to it:# Linux: the unversioned symlink ships with libclang-dev export LIBCLANG_PATH=$(llvm-config --libdir 2>/dev/null || echo /usr/lib/llvm-18/lib) # macOS (Homebrew llvm is keg-only) export LIBCLANG_PATH="$(brew --prefix llvm)/lib"
-
fatal error: zlib.h / bzlib.h / lzma.h: No such file or directory— the compression dev headers are missing (the runtime.soalone is not enough). Installzlib1g-dev libbz2-dev liblzma-dev(Debian/Ubuntu) orbrew install xz bzip2 zlib(macOS). -
error: linker 'cc' not found— no C toolchain. Installbuild-essential(Debian/Ubuntu) or Xcode Command Line Tools (xcode-select --install) on macOS. -
failed to run custom build command for 'hdf5-sys'— you passed--features hdf5withoutlibhdf5on the host. Installlibhdf5-dev(Debian/Ubuntu) orbrew install hdf5, or drop the feature.
If a build fails after fixing a prerequisite, re-run with a clean rebuild of the
C bits: cargo install --git ... faba --force.
faba <COMMAND> [OPTIONS]| Command | Purpose |
|---|---|
| Feature profiling — BAM → per-cell features | |
dartseq (dart, m6a) |
Call DART-seq m6A sites by a WT-vs-MUT control contrast on C-to-T conversions |
atoi (a2i, editing) |
Detect and quantify A-to-I RNA editing sites |
apa (polya) |
Quantify alternative polyadenylation sites per cell |
count (genes) |
Count reads per gene and call cells (single-cell or bulk RNA-seq) |
depth (rd) |
Compute read depth over genomic intervals |
snp (genotype) |
Discover and genotype SNP variants from BAM pileup |
run |
Set up the pipeline in a view (inputs, steps, flags), preview and save the command, and run it: SNP → count → ATOI → m6A → APA |
| QC — choose and apply thresholds after profiling | |
qc |
Pick thresholds in a full-screen view, then filter a faba output directory into a new fileset |
| Clustering: cells by their own sites | |
cluster |
Cluster cells by their site reads: cell and gene modules with a methylation shift per pair, fitted by variational inference. --modality count fits cell modules by gene counts instead; --within its run record then fits the sites within each of those cell modules |
cluster-view (modules, blocks) |
View a cluster fit from its run record {m}_blockmodel.run.json, or from its input directory (it never fits: run cluster first): its gene modules against its cell modules, each module's sites, the genes you pin side by side, and a gene's full pileup, one track per cell module; --html writes it as one self-contained page to share. A --within record ({m}_within.run.json) opens as the list of its fits, the count fit first; --group C3 opens the one within a cell module |
| Inspection & reference | |
pwm |
Build a position weight matrix around genomic sites |
pileup (inspect) |
ASCII pileup, or a faceted Miami plot, for one gene |
metagene (mg) |
Metagene histogram of site positions across gene features |
docs |
Print the method write-ups compiled into this binary |
mcp |
Serve the commands to an agent over the Model Context Protocol |
attach, sessions, kill, rename |
Return to, list, stop and label the views' detached sessions |
Run faba <COMMAND> --help for the detailed options of each subcommand.
faba mcp serves the commands as Model Context Protocol tools over stdio, so any MCP client can drive them, with any model behind it (Claude, GPT, Gemini, or a local one). faba sends nothing anywhere itself: the model sees what the tools return, and where that goes is up to the client and model you choose.
The server is one command, faba mcp. A client also needs to know where the project is: the server keeps its records (below) in its working directory, or in LEGUME_MCP_PROJECT when set. Set it whenever the client may start servers elsewhere, as desktop apps usually do. If the client cannot find faba, give its full path (which faba).
Most clients (Claude Desktop, Cursor's .cursor/mcp.json, Gemini CLI's settings.json, and others) take this shape:
{
"mcpServers": {
"faba": {
"command": "faba",
"args": ["mcp"],
"env": { "LEGUME_MCP_PROJECT": "/path/to/project" }
}
}
}Claude Code: claude mcp add faba -e LEGUME_MCP_PROJECT="$PWD" -- faba mcp. VS Code takes "servers" with "type": "stdio" in .vscode/mcp.json; Codex CLI takes [mcp_servers.faba] with command, args and env in ~/.codex/config.toml. Key names differ between clients; the server needs only the command, its argument and, optionally, the environment.
The server tells the agent how to work when it connects (the MCP instructions). Some clients do not pass those to the model; with one of them, put this in the agent's own instructions (an AGENTS.md, a system prompt):
faba turns coordinate-sorted BAM alignments into per-cell feature matrices: gene counts, read depth, A-to-I editing, DART-seq m6A, alternative polyadenylation and SNP genotypes. Row names are {gene_key}/{modality}/{detail}.
Producers write every candidate and apply no p-value cutoff. qc is where cells, features and sites are cut, and it writes a new directory.
1. docs lists the method write-ups. With `query` it prints the sections named (a topic such as profiling, a command such as qc, or a section number).
2. count, atoi, dartseq, apa, depth and snp each write one modality. run writes the pipeline (SNP, then count, A-to-I, m6A, APA) into one directory. dartseq needs control BAMs; without them, run skips m6A.
3. qc filters a directory into a new one, with the site thresholds given. It does not edit the input. With `dry_run` it writes nothing and returns what the cell QC keeps and what each threshold removes: try thresholds that way first.
4. pileup, metagene and pwm read a directory's sites. metagene and pwm return their table and its figure as an image. pileup returns a binned table for a gene or region (and, with `figure`, a Miami SVG, as an image too). cluster_view shows a cluster fit level by level, from its run record ({m}_blockmodel.run.json) or the directory holding its tables: the modules, then a gene module, a gene, the cell modules, the trace and verdict, the settings.
5. cluster fits cells by their own site reads (a blockmodel of cell and gene modules, by variational inference) and writes its tables to `out`. In two rounds: first `modality` count, cell modules by gene counts (it writes count_blockmodel.run.json); then, with `within` naming that record, m6a or atoi within each of its cell modules. The second round writes {m}_within.run.json, which cluster_view opens: the list of its fits, and with `group` the fit within one cell module.
The producers (count, depth, atoi, dartseq, apa, snp, run, qc, cluster) take `background`: on large BAMs set it, and the call returns a job id at once.
Every write goes to `out`, which must not exist. Leave `out` out and the output goes in today's results folder, named after the input and the tool. Paths resolve against the server's working directory. Common flags have their own arguments; further flags go in `options`, as `faba <command> --help` lists them; `options` cannot name the output or open a view. Results give the paths written and what the run recorded (`results`: a fit's ELBO verdict, its cluster sizes).
legume-mcp adds where the records are, the notebook tool (open with a plan: the question, the data, how the result will be checked) and the background jobs below.
| Tool | Does |
|---|---|
docs |
the method write-ups |
count, atoi, dartseq, apa, depth, snp |
one modality each, into a new directory |
run |
the pipeline (SNP, then count, A-to-I, m6A, APA) into one directory |
qc |
filters a directory into a new one |
cluster |
fits cells by their own site reads, by gene counts (modality count), or by site reads within a count fit's cell modules (within); writes its tables to a new directory |
pileup, metagene, pwm, cluster_view |
read what was written |
notebook, job_status, job_wait, job_cancel |
from legume-mcp |
On large BAMs a producer can outlast a client's wait on one call. Give it background: true and it returns at once with {job, written}; the agent follows the job with job_wait or job_status (or stops it with job_cancel), and uses written only once it is done. A job that succeeds is recorded like any step. Jobs end with their server: a run longer than a client session is for faba on the command line.
Every write goes to an out that must not exist yet: no tool replaces a directory or a file, and no tool opens a view. The server keeps the project's records the way legume-mcp lays them out, after Noble (2009), A quick guide to organizing computational biology projects, PLoS Comput Biol 5(7):e1000424: every step that writes goes into the day's results/<date>/runall.sh, which replays the session, and into results/INDEX.tsv; outputs the agent does not name go in results/<date>/. LEGUME_MCP_RECORDS=off turns the records off. While an agent works, the server also writes a live feed a person can follow and steer (stop a call or a job, leave the agent a note) with legume-mcp's watch; LEGUME_MCP_WATCH=off writes none.
faba cluster (cell and gene modules of the site reads) fits by variational inference, as the
count fit does: each restart (--chains, 4) starts from k-means++ on the cells' residuals over
their genes, runs --svi-epochs minibatch epochs, then coordinate ascent until an epoch moves the
ELBO by less than --tol per read, and the best restart by ELBO is kept. A read's likelihood is
bounded by Pólya-Gamma (Jaakkola and Jordan), one bound per read and block, so each link (a cell's
reads at one gene) weighs every block at a few sums; the shifts and the cells' levels trade a
constant through their priors after every global step. On the paper data (11,919 cells, 2.35M
links) a restart takes about two minutes. The MCMC fits faba had before 0.19 (--model link and
crp, the sampler's --sweeps and --burn-in, --holdout) are gone, and their flags are
refused by name; their records still open in cluster-view.
# Cell and gene modules from a faba output directory, watching the restarts as they run
faba cluster out/
# Open the fit, as it is
faba cluster-view out/m6a_blockmodel.run.jsonIn two rounds. Counts and site reads are not fitted jointly: a cell has far more UMIs than site reads, so the counts would decide the modules alone. Instead the cells are first clustered by their gene counts, and the sites are then fitted within each of those cell modules:
# Round 1: cell modules by gene counts (writes out/count_blockmodel.run.json)
faba cluster out/ --modality count
# Round 2: m6A (or A-to-I: --modality atoi) within each count cell module
faba cluster --within out/count_blockmodel.run.json
# The count fit and the fits within it; --group C3 opens one of them
faba cluster-view out/m6a_within.run.jsonThe count fit is by variational inference (after Gopalan, Hofman and Blei's hierarchical Poisson
factorization): each restart (--chains, 4) starts from k-means++ on the cells' normalized
profiles, then the genes' profiles over those clusters, then runs online minibatch epochs (--svi-epochs 10 of
--batch-size 1000 cells), then coordinate ascent until an epoch moves the ELBO by less than
--tol (1e-5) per count, at most --epochs (200) in all. Only nonzero counts are visited, and
nothing cells × genes is held dense. The tables are the best restart's by ELBO; the trace is each
restart's ELBO by epoch, and the verdict says whether the best one settled.
The second round reads the site matrices beside the count fit's inputs, unless some are named,
and matches cells by batch and barcode. Each cell goes to its main count module (C1, C2, …). A
group with fewer than --within-min-cells (50) cells to fit is skipped. Each group's fit is an
ordinary one under within/{m}/{group}/, with its
own run record and verdict; {m}_within.run.json lists them, and {m}_within_cells.parquet
gives each cell its group and its site module there. cluster-view --html fit.html writes the
count fit's page and fit.{group}.html beside it for each fitted group, linked to each other.
Batch effects, after the fit. Batches fitted together can come out as modules of their own.
cluster-view reads each cell module's spread over the batches off the cell table, each batch's
cells weighing alike, with the batches as replicates of conditions: a batch's name less its
replicate token (rep1_mut and mut_r2 are replicates of mut; a folder, runA/mut, is one
too), or as --batch-groups rep1_mut=mut,... says. A module even over each condition's
replicates (normalized entropy at least 0.8, each condition weighing alike however many
replicates it has) is shared, or that condition's when 80% of it is in one condition; uneven, it
is batch-specific, as a condition's replicates should agree. A module mostly where no condition
has two replicates, or with under 10 cells' worth, is not judged. In the browser, b opens the modules against the batches
and B has the panes show the modules that are not batch-specific, then those that are, then
all; --batches prints the same.
faba cluster-view out/count_blockmodel.run.json --batchesGO terms of the gene modules. cluster-view --go tests each gene module's genes (those whose
main gene module it is, at a weight of at least --go-min-weight, 0.5) against the fit's own genes
that GO annotates: the hypergeometric tail of the overlap, Haldane's odds ratio, and BH over every
(module, term) pair, for terms of 5 to 500 annotated genes, propagated up the GO graph (is_a,
part_of) with NAS and ND evidence left out. A module's terms are listed by p, a parent left out
when it holds the same genes. The annotations default to lupin's cached GO Consortium file for the
genes' species (human or mouse); --gaf and --obo name others, --gmt any gene sets. In the
browser, a opens the gene module in focus's terms (↑/↓ steps the module); MCP's cluster_view
takes go.
faba cluster-view out/m6a_blockmodel.run.json --goWatching the restarts. On a terminal faba cluster draws each restart's ELBO live while it
fits (q cancels, Ctrl+C stops the restarts early and writes what they reached). When it is done
it prints the same traces as one sparkline per restart on one scale, then a verdict and one line
of advice naming the flags to run again with. Once the tables are open, the browser's header
carries an ELBO ... ok or check badge, and t opens the trace view; --html pages carry the
trace chart too. Each restart's trace is kept in {m}_trace.parquet.
Judging convergence. A fit has converged when the best restart's ELBO settled within --tol
per unit (count or read) before --epochs ran out; the verdict also gives the spread of the
restarts' ELBO per unit, how far apart their optima are. {m}_blockmodel.run.json keeps each
restart's ELBO and epochs, the best, and whether it settled, under results.fit. When it has not
settled, run again with more --epochs; restarts far apart point to more roles than the data
support: try fewer --cell-roles or --gene-roles, or more --chains.
Every figure faba writes takes its size on the page in inches: --fig-width and --fig-height,
either or both (the other is then the figure's own). It is laid out for that size, its text set in
points as it prints, rather than scaled. This holds for cluster-view --figure (where 0.19.0's
--width and --height still work), metagene --figure, pwm --figure, qc --plots, and
pileup's Miami plot (8 inches wide unless given; its tracks share the height given). Over MCP,
each of these tools takes width and height. A size too small for a figure's labels is
refused, saying the least it needs.
# A column-wide modules grid and a page-wide logo
faba cluster-view out/m6a_blockmodel.run.json --figure grid --fig-width 3.5 --fig-height 3
faba pwm out/ -f genome.fa --batch-process --figure logo --fig-width 7The method write-ups are compiled into the binary — include_str!, not files read at
runtime — so they travel with a cargo installed faba or one copied to a cluster with no
checkout beside it, and the build fails if one of them goes missing.
faba docs # list what there is
faba docs profiling # BAM -> per-cell features: m6A, A-to-I, APA, counts, SNPsThe same files live in docs/, which also carries the design notes for work that is
planned but not implemented (kept separate on purpose — reading a plan as though it described
the code is how people end up debugging things that were never built).
# Gene counts and cell calling from a single-cell BAM
faba count sample.bam -g genes.gff -o out/
# A-to-I editing sites (every putative site is written; `faba qc` decides)
faba atoi sample.bam -g genes.gff -f genome.fa -o out/
# DART-seq m6A: signal (WT APOBEC1-YTH) vs catalytically-dead control (YTHmut).
# A control is REQUIRED — m6A can't be told apart from genomic C/T variation
# without it (--mut / --control / --background are accepted aliases).
faba dartseq wt.bam --control-bam ctrl.bam -g genes.gff -f genome.fa -o out/
# Everything in one pass (the m6A step runs only when --control-bam is given,
# otherwise it is skipped; the other steps need no control).
# Set up and run everything in a view (plain `faba run`), or straight through:
faba run
faba run --batch-process sample.bam -g genes.gff -f genome.fa -o out/ --control-bam ctrl.bam
# The producers apply no p-value / effect-size / reproducibility cutoff.
# Pick the thresholds in a full-screen view, then cut into a NEW directory
# (plain `faba qc` asks for both directories in a pop-up):
faba qc out/ -o out_qc/
# ...or, in a script, cut with the thresholds as given:
faba qc out/ -o out_qc/ --batch-process --site-max-pv 0.05 --site-min-cells 10 --auto-cutoffMIT — see the workspace license.



