YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
mgxLens
A retrieval-based metagenomic taxonomic profiler. Given short DNA reads (~150 bp) from a sequencing run, it reports the estimated relative abundance of microbial taxa at the genus and species level.
Reference coverage. The index covers 13,399 distinct species-level SGBs across 3,297 genera β the full MetaPhlAn vJan25 SGB reference. Every indexed SGB resolves to a named Linnaean species (no unnamed placeholder bins among the indexed organisms; the naming rate over indexed SGBs is 100%). This means "closed-world" is about organisms outside MetaPhlAn's reference entirely β not gaps within it. An organism that is in MetaPhlAn's reference is representable; one that is not will be misassigned to its nearest reference genus (see Caveats β closed-world).
1. What's in the bundle
| File | What it is |
|---|---|
encoder.pt |
The neural encoder (seqLens-89M backbone + attention pooling β 256-d embedding). Turns a DNA string into a vector. |
index.faiss |
A FAISS vector index of ~2.32M reference marker embeddings. Cosine search via inner product. |
index.clades.npy |
Row-aligned to the FAISS index: the SGB (clade) id for each indexed vector. |
index.markers.npy |
Row-aligned marker source ids (provenance; not needed for inference). |
index.config.json |
Index build parameters (window=150, nwin=5, emb_dim=256, max_length=128). |
lineage.tsv |
clade_id β genus, species, display_name. Turns a retrieved SGB into a taxon name (display_name is MetaPhlAn-style). |
mgx_encoder.py |
The encoder class + load_encoder() / embed() helpers. Import this; don't reimplement. |
MANIFEST.json |
Machine-readable summary + file checksums + caveats. |
Encoder and index are from the same training run (index.config.json.source_ckpt ==
encoder.pt). Do not mix this encoder with any other index or vice-versa β the vectors would be
meaningless.
2. Environment
pip install torch transformers faiss-cpu numpy
# GPU optional: faiss-gpu + a CUDA torch build speed up large batches, but faiss-cpu is fine for
# moderate throughput. The encoder runs on CPU or GPU; GPU is ~10-50x faster for embedding.
The encoder downloads its base model (omicseye/seqLens_4096_512_89M-at-base-multi) from
HuggingFace on first load via transformers, so the serving host needs network access on first
run (or pre-cache the HF model). trust_remote_code=True is required for that base model.
3. How it works (the inference recipe)
For each input read:
- Window the read to 150 bp (if longer, take a centered 150 bp window; if β€150, use as-is).
- Embed it with the encoder β a 256-d L2-normalized vector.
- Search the FAISS index for the top-k nearest reference vectors (k=25). Because vectors are L2-normalized and the index is inner-product, the score is cosine similarity β [-1, 1].
- Gate: if the top-1 cosine is below a threshold Ο (start with 0.66), the read is abstained (dropped, counted as "unclassified"). Otherwise it's assigned.
- Assign: the read's taxon is the clade (
index.clades.npy) of its top-1 hit. Map that clade to genus/species vialineage.tsv. - Aggregate across all assigned reads: count reads per genus (and per species), normalize to fractions β the abundance profile. Report the unclassified fraction separately.
That's the whole pipeline. It's nearest-neighbor retrieval, not a trained classifier.
4. Reference implementation (copy-paste starting point)
import numpy as np, faiss, torch
from collections import defaultdict
from mgx_encoder import load_encoder, embed # ships in this bundle
BUNDLE = "." # path to the bundle dir
K = 25 # neighbors per read
GATE = 0.66 # top-1 cosine threshold; below -> abstain
WINDOW = 150
# ---- load once at startup ----
device = "cuda" if torch.cuda.is_available() else "cpu"
model, tok, cfg = load_encoder(f"{BUNDLE}/encoder.pt", device)
index = faiss.read_index(f"{BUNDLE}/index.faiss")
clades = np.load(f"{BUNDLE}/index.clades.npy", allow_pickle=True) # (N,) SGB id per vector
lineage = {} # clade_id -> (genus, species, display_name)
with open(f"{BUNDLE}/lineage.tsv") as fh:
next(fh)
for line in fh:
cid, g, s, disp = line.rstrip("\n").split("\t")
lineage[cid] = (g, s, disp)
def center_window(seq, w=WINDOW):
if len(seq) <= w:
return seq
s = (len(seq) - w) // 2
return seq[s:s+w]
def profile(reads, batch_size=4096):
"""reads: list of DNA strings. Returns genus/species abundance + unclassified fraction.
Species keys are MetaPhlAn-style display names ('Escherichia coli', or
'Escherichia sp. (SGB123)' for unnamed genome bins)."""
genus_counts, species_counts = defaultdict(float), defaultdict(float)
n_total = len(reads); n_assigned = 0
for i in range(0, n_total, batch_size):
batch = [center_window(r) for r in reads[i:i+batch_size]]
ml = cfg["max_length"] if "max_length" in cfg else 128
Z = embed(model, tok, batch, device, max_length=ml).astype("float32")
D, I = index.search(Z, K) # D=cosine sims, I=index rows
for r in range(len(batch)):
if D[r, 0] < GATE: # gate: abstain
continue
clade = str(clades[I[r, 0]]) # top-1 hit's clade
g, s, disp = lineage.get(clade, ("", "", ""))
if not g:
continue
n_assigned += 1
genus_counts[g] += 1
if disp:
species_counts[disp] += 1 # display name, not raw s__ / SGB id
def norm(d):
tot = sum(d.values()) or 1.0
return {k: v/tot for k, v in sorted(d.items(), key=lambda x: -x[1])}
return {
"genus": norm(genus_counts),
"species": norm(species_counts),
"n_reads": n_total,
"n_assigned": n_assigned,
"unclassified_fraction": 1.0 - (n_assigned / n_total if n_total else 0.0),
}
# ---- example ----
if __name__ == "__main__":
reads = ["ACGT..."] # your reads (from a FASTQ)
print(profile(reads))
Reading FASTQ: reads come from .fastq/.fastq.gz. Use pysam, Bio.SeqIO, or a
2nd/4th-line parser to get the sequence strings, then pass the list to profile(). Paired-end
R1/R2 can both be fed as independent reads.
5. Serving it (FastAPI sketch)
from fastapi import FastAPI, UploadFile
app = FastAPI()
# model/index/lineage loaded once at module import (see section 4)
@app.post("/profile")
async def profile_endpoint(file: UploadFile):
reads = parse_fastq(await file.read()) # your FASTQ parser -> list[str]
return profile(reads) # JSON: {genus:{...}, species:{...}, ...}
Load the model, index, and lineage once at startup (they're large β encoder 342 MB, index 2.3 GB). Never reload per request. The index holds ~2.3 GB in RAM; size the host accordingly (β₯8 GB RAM recommended, β₯16 GB comfortable).
6. Input / output contract
Input: DNA reads as strings (from FASTQ), ~150 bp each. Non-ACGT characters are tolerated by the tokenizer but degrade the embedding; upstream QC/trimming (e.g. fastp) is assumed.
Output (JSON):
{
"genus": {"escherichia": 0.29, "pseudomonas": 0.24, "...": 0.0},
"species": {"Escherichia coli": 0.21, "Pseudomonas aeruginosa": 0.18, "...": 0.0},
"n_reads": 200000,
"n_assigned": 94459,
"unclassified_fraction": 0.528
}
Abundances are fractions summing to ~1.0 within each level (over assigned reads). All 13,399
indexed SGBs have real species names, so species keys are Linnaean names (the Genus sp. (SGBxxxx)
fallback only appears if the index is rebuilt to include unnamed bins).
7. Tunable knobs
| Knob | Default | Effect |
|---|---|---|
GATE (top-1 cosine) |
0.66 | Higher = stricter, more reads abstained, fewer false assignments (but see Caveats β it does not reliably reject novel organisms). |
K (neighbors) |
25 | Only top-1 is used for assignment here; k>1 matters if you switch to k-NN vote aggregation. |
| aggregation | top-1 vote | Simplest. Alternatives (k-NN vote, similarity-weighted) exist in the research code but top-1 is the documented default. |
8. Caveats β read before building product on this
- Proof of concept. Not accuracy-tuned. The goal of this handoff is to enable infrastructure and integration work, not to ship a validated diagnostic.
- Genus is the reliable level; species is approximate. At 150 bp, reads from different species in the same genus are near-indistinguishable to the encoder β the model reliably gets the genus right but frequently assigns the wrong species within that genus. Report species output, but do not treat it as trustworthy.
- Species naming. All 13,399 indexed SGBs have real Linnaean species names, so normal output
shows names like "Escherichia coli", not bin ids. (The
display_namelogic still falls back toGenus sp. (SGBxxxx)for unnamed bins β this only matters if the index is ever rebuilt to include the unnamed SGBs that exist elsewhere in the MetaPhlAn taxonomy; the shipped index has none.) - Closed-world β this is the big one. The model can only assign a read to a taxon that exists in its reference index. An organism that is not in the reference will still be confidently assigned to its nearest reference genus and reported as present. There is effectively no novelty rejection (abstain rate ~0% in evaluation), and this behavior was never quantified. Do not deploy this against arbitrary/open real-world samples and present the output as complete or trustworthy without adding a novelty-rejection layer. It is validated only in the closed-world setting where the sample's organisms are known to be in the reference.
- Evaluated on one mock community (Zymo D6300). Broader validation was not done.
9. Provenance
- Encoder:
enc_attention_bg1.best_genus.ptβ seqLens-89M backbone, attention pooling, 256-d projection, trained with contrastive InfoNCE + background negatives, checkpoint selected on real-Zymo genus accuracy. - Index:
index_bakeoff_mw_notestβ leakage-fixed (held-out test markers excluded). - Base model pulled at runtime:
omicseye/seqLens_4096_512_89M-at-base-multi.
See MANIFEST.json for file checksums and the machine-readable summary.