Reproducible benchmark of RaPDTool against read/marker-based classifiers (Kraken2 (+Bracken) and MetaPhlAn4; mOTUs is wired in as an optional comparator but was not part of the reported runs) for the manuscript. It measures footprint (DB size, peak RAM, wall time), profiling accuracy (OPAL vs a gold standard), and genome-recovery / type-material capabilities that read classifiers do not provide.
Analysis code, protocol, community-defining inputs, and summary result files for the RaPDTool benchmark. Configure paths in
config.sh(see Configuration), then follow Steps 0 → 6.
Reading order. For why the benchmark is designed this way — the flow diagram,
what each test measures, and the scope the design covers and deliberately does not —
read benchmark_rationale.md first. This file is the operational how-to: follow
Steps 0 → 6 below and a reviewer can reproduce every number in the manuscript.
git clone https://github.com/BioTools-Dev/rapdtool-benchmark.git
cd rapdtool-benchmarkEvery command in this file is run from that directory. If you are reading this from the
Zenodo archive or a supplementary ZIP instead, unpack it and cd into it — it is the
same tree (the archive is a tagged snapshot of the repository).
Order of operations, starting cold:
| # | Do this | Section | Time |
|---|---|---|---|
| 1 | git clone |
above | 1 min |
| 2 | Install the tools (two conda envs) | Prerequisites → Getting started A | ~20 min |
| 3 | cp config.sh.example config.sh, edit it, source config.sh |
Configuration | ~10 min |
| 4 | scripts/verify_kit.sh — reproduces every published figure and number |
Configuration | ~2 min |
| 5 | Download the databases, rebuild the reads, re-run every tool | Getting started B–C → Steps 0 → 6 | days |
Steps 1–4 are the reproduction most readers want, and need no large database — only a taxonomy dump. Step 5 re-derives everything from raw reads and needs ~300 GB of reference data.
The clone ships the published figures and the small summary result files. That is
deliberate: they are the reference outputs — the answer key your own run is checked
against, not a shortcut around it. scripts/verify_kit.sh regenerates each figure from
the shipped inputs and compares it byte-for-byte against the committed PNG. Without a
committed copy to diff against, a regenerated figure would just be a picture, with no way
to tell whether it is the right one. (PNG carries no timestamp, so an exact byte match is
strong evidence of an exact reproduction; SVG and PDF embed a creation date and are
therefore not byte-comparable.)
| Shipped in the clone | You regenerate |
|---|---|
figures/ — the five manuscript figures |
simulated reads (~36 GB) and MEGAHIT assemblies |
results/*/ — CAMI profiles, OPAL results.tsv, summary.csv, mash confidence and miComplete tables |
raw Kraken2 / Bracken / MetaPhlAn output |
data/ — genome lists, abundance vectors, the 30,209-genome census |
the reference databases (~300 GB, Getting started B) |
Nothing you are asked to reproduce is handed to you in a way that could hide a failure:
you rebuild the artefact and the kit tells you whether your bytes match. Overwriting the
committed figures is expected — git diff --stat after a run is itself a result. To put
them back: git checkout -- figures/.
rapdtool-benchmark/
*.md docs: README (how-to), benchmark_rationale (why), mock_design (the 20 genomes)
scripts/ all analysis scripts (.py, .sh)
data/ inputs: genome lists, abundance vectors, the census TSV, flow diagram
results/ bench_<dataset>/ tool outputs, profiles, OPAL results
figures/ generated figures (SVG + PNG 300 dpi + PDF) — manuscript deliverables
Run every command from this base directory (e.g. scripts/mash_detection.py …,
python3 scripts/plot_census.py). Scripts resolve data/, results/ and figures/
relative to it; the shell scripts cd here themselves.
External paths (reference databases, tool locations) are read from environment variables, not hardcoded. Once, before running anything that touches a database:
cp config.sh.example config.sh # then edit config.sh with your paths
source config.shconfig.sh is git-ignored; only the template is committed, and every variable in it is
commented with what reads it and when.
You do not have to fill in all of them to get started. For the figure-and-number
reproduction (verify_kit.sh, and level 1 below) the only variable that must be real is
TAXONKIT_DB, a local NCBI taxdump — everything else can keep its placeholder until
you attempt the full re-run:
# minimum viable config: a pinned NCBI taxonomy dump (~450 MB)
mkdir -p ~/taxonkit_db && cd ~/taxonkit_db
wget https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump_archive/taxdmp_2026-07-10.zip
unzip -o taxdmp_2026-07-10.zip # then set TAXONKIT_DB=~/taxonkit_db in config.shStart here — one command that checks the whole thing:
source config.sh
scripts/verify_kit.shIt regenerates the five figures and compares them byte-for-byte against the committed PNGs, re-runs the threshold sweep and the detection numbers, and recomputes every census figure quoted in the manuscript — about two minutes, no large database required. Run it before anything else: it tells you whether your environment reproduces the study before you spend days on the full re-run.
Expected output with only TAXONKIT_DB set:
19 passed, 0 failed, 1 skipped — the skip is the optional census rebuild, which
needs the competitor databases. Once $KRAKEN_STD_INSPECT / $MPA_PKL / $FOCUS_DB
are also configured it rebuilds the census from those databases and diffs it against the
shipped one, and you get 20 passed, 0 failed, 0 skipped.
figures/ is backed up and restored, so the check leaves a clean checkout clean —
git status after it should show nothing.
Byte-identical PNGs require the same matplotlib. This study used matplotlib 3.11.0 on Python 3.13. A different version usually renders a visually identical figure whose bytes differ, which shows up as a section-2
FAILwhile every number in sections 3–5 still passes. That combination means your numbers reproduce and your renderer differs — not a failure to reproduce the study.
Two levels of reproduction:
- Verify the figures and reported numbers from the shipped summaries. The small
result files under
results/(CAMI profiles, OPALresults.tsv,summary.csv, mash confidence tables, miComplete tables) and the inputs underdata/are included, so the plotting and detection scripts regenerate every figure and number directly. This needs only$TAXONKIT_DB(a pinned NCBI taxdump) for the taxid rollups:(source config.sh # the five figure scripts — each regenerates from the shipped result files alone: python3 scripts/plot_census.py -i data/census_full.tsv -l data/mock_genomes.list \ -o figures/census # Fig 2 -> figures/census.* python3 scripts/plot_opal_depth.py # Fig 3 -> figures/opal_depth.* python3 scripts/plot_micomplete.py # Fig 4 -> figures/micomplete.* python3 scripts/plot_mirror_distance.py # Fig 5 -> figures/mirror_distance.* python3 scripts/plot_f1_threshold.py # threshold analysis -> figures/f1_threshold.* # detection numbers (recall / precision / F1) from the mash confidence table: python3 scripts/mash_detection.py --bench results/bench_ln_30M \ --gold results/bench_ln_30M/gold_standard.profile --split data/mock_genomes.list
scripts/threshold_sweep.py --truth <gold_standard.profile>writes the precision–recall sweep,figures/threshold_sweep.*; the flow diagram is rendered fromdata/benchmark_flow.dotwithdot.) - Full re-run from scratch. Regenerate the simulated reads
(
scripts/make_mock.sh … --seed 42, from the shipped genome lists and abundance vectors), run the tools (scripts/run_benchmark.sh), and re-derive everything. This needs the reference databases named inconfig.sh(Kraken2 ≈104 GB, MetaPhlAn ≈60 GB, RaPDTool ≈0.5 GB) and the ZymoBIOMICS reads (ENA PRJEB29504). Large regenerable data (reads, assemblies, raw Kraken/Bracken output) are not shipped — they are git-ignored and reproduced from the recipe.
Large read/assembly data live outside the kit at $MOCK_ROOT/mock_* and $ZYMO_DIR.
| File | Purpose |
|---|---|
benchmark_rationale.md |
why the benchmark is shaped this way — flow diagram, what each test measures, and the scope of the design. Read this first. |
check_representation.py |
census a genome set against Kraken2 (full/16/8) and MetaPhlAn4: is each species in each database? Produces both the population census and the mock genome selection |
data/census_full.tsv |
the census of all 30,209 type-material genomes (Phase 0 result) |
plot_census.py / figures/census.* |
census figure (PNG 300 dpi + SVG + PDF) |
data/mock_genomes.list / mock_design.md |
the 20 selected genomes (10 reference + 10 conflictive) and the sampling rationale |
make_mirror_set.py |
select the mirror set: species Kraken2 has and RaPDTool lacks, to characterise out-of-domain failure mode |
data/mirror_genomes.tsv / .list / data/mirror_pool.tsv |
the 10 selected mirror genomes, and the full 9,640-species pool they came from |
make_abundance.py |
build a fixed per-contig abundance vector — required, see the InSilicoSeq caveat below |
make_mirror_distance.py / data/mirror_distance.tsv / data/mirror_dist_genomes.list |
distance-stratified mirror: sample across genomic distance, measure Mash distance to RaPDTool's DB |
plot_mirror_distance.py / figures/mirror_distance.* |
rank-resolution-vs-distance figure |
data/mock_abundance.txt / data/mock_abundance_equalcov.txt / data/mirror_dist_abundance.txt |
the vectors actually used (uneven 20×; equal-coverage; mirror equal-coverage) |
make_mock.sh |
build a mock community: simulate reads (iss) → assemble (MEGAHIT) → write the mock_composition.tsv gold standard |
run_benchmark.sh |
run each tool on the same input/hardware; record RAM, time, DB sizes; report medians |
setup_taxdb.py |
pin the NCBI taxonomy dump (build an ete3 SQLite) for reproducible name/taxid resolution |
profile2cami.py |
convert FOCUS / Bracken / Kraken2 / MetaPhlAn profiles and a gold standard to CAMI/BIOBOXES for OPAL (taxonkit or ete3 backend) |
mash_detection.py |
RaPDTool species detection from the mash confidence table (recall/precision/F1 + ref/conflictive split); detection = mash, abundance = FOCUS |
run_opal.sh / run_opal_zymo.sh |
convert every profile (matching SampleID) and run OPAL, per dataset; the Zymo variant handles the sequence- vs cell-abundance split |
run_metawrap.sh |
MAG-recovery comparison vs MetaWRAP on the same assembly: binning + bin_refinement, then re-scores both bin sets with the same evaluator (miComplete/Bact105) → results/metawrap/ (Step 5d, rationale §4d) |
threshold_sweep.py / figures/threshold_sweep.* |
precision–recall vs abundance cutoff (reads the CAMI profiles; picks the operating point) |
plot_f1_threshold.py / figures/f1_threshold.* |
F1 vs a uniform abundance threshold — the MetaPhlAn crossover |
plot_opal_depth.py / figures/opal_depth.* |
profiling accuracy (Bray–Curtis, F1) vs sequencing depth |
verify_kit.sh |
one-command check that this kit reproduces the published figures and numbers on your machine (~2 min, no large DB needed) — run it first |
config.sh.example |
template of external paths (databases, tools); copy to config.sh and edit |
README.md |
this file — the operational step-by-step |
| Dataset | What it is | Answers |
|---|---|---|
| (census, no reads) | all 30,209 type-material genomes vs each competitor DB | what each database contains |
mock_ln_3M |
20 genomes, uneven (20× range), 3 M reads | depth response, low |
mock_ln_10M |
same vector, 10 M reads | depth response, mid |
mock_ln_30M |
same vector, 30 M reads | depth response, high |
mock_equalcov_30M |
20 genomes, equal coverage (38.8×), 30 M reads | capability with coverage removed as a limit |
mock_mirrordist_15M |
14 genomes spanning genomic distance to RaPDTool's DB (100 % → ~70 % id), equal coverage (27.7×), 15 M reads | out-of-domain rank resolution vs distance: does it degrade gracefully? |
ZymoBIOMICS_db |
real Illumina data, community defined by a third party | does it work on data we did not simulate? |
The three ln sets share one fixed abundance vector so that depth is the only
variable between them. Locations: simulated mocks under $MOCK_ROOT/mock_*, real data
under $ZYMO_DIR, mirror genomes downloaded to $MIRROR_FNA_DIR.
Together they answer four different questions, and none substitutes for another:
coverage of the databases (census), performance under realistic unevenness (depth
series), capability when coverage is not limiting (equal-coverage control), safety
outside the intended domain (mirror), and external validity (Zymo). See
benchmark_rationale.md for why each exists.
- Python 3 with matplotlib 3.11.0 — every figure script, and the byte-for-byte
figure check in
verify_kit.sh, depends on the renderer version. - The tools you compare, reachable by absolute path (see the warning in Step 1 —
do not rely on
PATH):rapdtool,kraken2,bracken,metaphlan(and/ormotus); GNU/usr/bin/time. - Accuracy step: OPAL (
cami-opal) and AMBER (cami-amber), installed in a separate Python 3.11 conda env (see A below — they pin numpy/pandas versions that fail to build on Python ≥3.13). - A taxonomy backend for
profile2cami.py(auto-selected):- taxonkit + a local NCBI dump — no pip install, no download. Already set up
here:
export TAXONKIT_DB=$TAXONKIT_DB(the dump the FOCUS DB was built from, 2026-07-10, sha256c1b91199…; 10/10 updated phyla + 100% of FOCUS output species resolve). Recommended. - or ete3 (
pip install ete3) with a pinned sqlite fromsetup_taxdb.py.
- taxonkit + a local NCBI dump — no pip install, no download. Already set up
here:
A. Install the tools (once). RaPDTool you already have; the rest go in a dedicated conda env:
# Tools env (any recent Python is fine here; the study used 3.13)
# matplotlib is pinned because the figure check compares PNGs byte-for-byte.
conda create -n rapdtool_bench -c conda-forge -c bioconda \
kraken2 bracken metaphlan insilicoseq megahit taxonkit matplotlib=3.11.0 -y
conda activate rapdtool_bench
# RaPDTool runs via its launcher wrapper; if `rapdtool` is not on PATH in this env,
# pass its absolute path as RAPDTOOL=... in Step 1 (e.g. .../RaPDTool/scripts/rapdtool)OPAL/AMBER go in a SEPARATE Python 3.11 env. cami-opal pins exact
numpy==2.0.1 / pandas==2.2.2, which have no wheels for Python ≥3.13 and then
fail to compile from source with new GCC. The OPAL wheel itself is …-py311-…, so
use 3.11 and pip installs prebuilt wheels (no compilation):
conda create -n opal -c conda-forge python=3.11 -y
conda activate opal
pip install cami-opal cami-amberprofile2cami.py needs neither numpy nor pandas (stdlib + taxonkit only), so run it
in rapdtool_bench; switch to the opal env only for the opal.py / amber.py
calls. (Add ete3 to whichever env runs profile2cami.py only if you are not
using the taxonkit backend.)
B. Download the databases.
- Kraken2 Standard (full, ~104 GB on disk), Standard-16 (~15 GB) and
Standard-8 (~7.5 GB) from the prebuilt indexes at
https://benlangmead.github.io/aws-indexes/k2 (the k2 tarballs already include the
Bracken
*.kmer_distribfiles). Full + the two capped sizes give the Kraken accuracy-vs-DB-size curve vs RaPDTool's 0.5 GB. Needs ~130 GB free disk; loading the full DB needs ~104 GB RAM. This run used the 2026-06-26 build (place it at$KRAKEN_STD_DBetc.):mkdir -p /path/to/kraken2_db/{standard,standard16,standard8} && cd /path/to/kraken2_db wget https://genome-idx.s3.amazonaws.com/kraken/k2_standard_20260626.tar.gz wget https://genome-idx.s3.amazonaws.com/kraken/k2_standard_16_GB_20260626.tar.gz wget https://genome-idx.s3.amazonaws.com/kraken/k2_standard_08_GB_20260626.tar.gz tar -xzf k2_standard_20260626.tar.gz -C standard tar -xzf k2_standard_16_GB_20260626.tar.gz -C standard16 tar -xzf k2_standard_08_GB_20260626.tar.gz -C standard8 - MetaPhlAn DB (~60 GB for the current
vJan26_CHOCOPhlAnSGBSGB build — ~21 GB marker.fna+ ~39 GB prebuilt Bowtie2 index.bt2l), once:metaphlan --install --db_dir ~/metaphlan_db(MetaPhlAn 4.1+ renamed--bowtie2db→--db_dir; pick any path with space, e.g.$MPA_DB. Then setMPA_DB=<that dir>inrun_benchmark.shCONFIG.) - RaPDTool DBs: already cached (
rapdtool --where).
C. Build the datasets. The 20 genomes are already selected in data/mock_genomes.list;
make_mock.sh simulates reads, assembles them, and writes the gold standard.
iss --abundance <dist>assigns abundance per FASTA record (contig), not per genome. With draft multi-contig genomes the realised per-genome abundance therefore tracks contig count, largely regardless of which distribution you request. Measured on this genome set with--abundance uniform: a 1,491-contig genome got 61.3 % of the reads and a single-contig genome got 0.04 % — a 1,000-fold coverage range from a request for uniform.Always pass a fixed vector with
-b, built bymake_abundance.py, which assigns per-genome fractions explicitly and splits each across that genome's contigs proportionally to length. Never rely on-a.
# run from the repository base directory
conda activate rapdtool_bench
# C.1 — build the two abundance vectors (fast, no reads involved)
scripts/make_abundance.py -l data/mock_genomes.list -o data/mock_abundance.txt --depths 3 10 30
scripts/make_abundance.py -l data/mock_genomes.list -o data/mock_abundance_equalcov.txt \
--equal-coverage --depths 30Both commands print the per-genome coverage they will produce at each depth — check that table before spending an hour on simulation.
# C.2 — the depth series: ONE vector, three depths (~30 min + assemblies)
for N in 3000000 10000000 30000000; do
scripts/make_mock.sh -o $MOCK_ROOT/mock_ln_$((N/1000000))M -t 16 -n $N \
-b data/mock_abundance.txt --seed 42 -l data/mock_genomes.list
done
# C.3 — the equal-coverage positive control (38.8x for all 20 genomes)
scripts/make_mock.sh -o $MOCK_ROOT/mock_equalcov_30M -t 16 -n 30000000 \
-b data/mock_abundance_equalcov.txt --seed 42 -l data/mock_genomes.listEach run produces, under its -o directory:
reads_R{1,2}.fastq, asm/final.contigs.fasta, mock_composition.tsv (the gold
standard), and make_mock.log.
Notes that will save you a debugging session:
-
Use
final.contigs.fasta, not MEGAHIT'sfinal.contigs.fa. The FOCUS version bundled in the RaPDTool container accepts only.fasta/.fna/.fastq; given.fait aborts with a misleadingNameError: name 'sys' is not definedthat hides the real message.make_mock.shwrites the.fastacopy for you. -
issdoes not writereads_abundance.txtwhen-bis used (the composition was yours to begin with).make_mock.shcopies your vector into place so the gold standard still builds. -
-nis total reads, not pairs:-n 30000000gives 15 M pairs. -
What is, and is not, bit-reproducible — verified by regenerating
mock_ln_3Mfrom this recipe and diffing it against the dataset the study used:Artefact Reproducibility reads_R{1,2}.fastq(InSilicoSeq,--seed 42)byte-identical mock_composition.tsv(gold standard)byte-identical data/mock_abundance*.txt(the vectors)byte-identical data/census_full.tsv(the census)byte-identical asm/final.contigs.fasta(MEGAHIT)same contig count and total length; 34,670 of 34,710 contigs byte-identical (99.9 %) — MEGAHIT's multithreaded local assembly is not deterministic, so ~0.1 % of contigs differ between runs Consequence: everything scored from reads (detection, the reference/conflictive split, resource medians) reproduces exactly, while numbers scored from the assembly (bin counts, miComplete completeness/redundancy) can shift marginally between re-runs. That is expected and is a property of MEGAHIT, not of this kit — do not chase a last-digit difference in a bin statistic.
-
--seedmakes the read simulation reproducible for a given-nand-b.
Database paths are passed as environment variables at run time (Step 1); the CONFIG
block in run_benchmark.sh holds only defaults, so you never edit the script.
How the 20-genome list was produced (and how to regenerate/customise it).
data/mock_genomes.list is not hand-waved — it was derived with check_representation.py,
which reads each competitor DB's own taxonomy (no downloads) and reports, per
genome species, presence in Kraken2 (full/16/8) and MetaPhlAn4. To reproduce it:
-
Confirm the DB paths the script reads. They come from the environment — i.e. from
config.sh, nothing in the script needs editing:KRAKEN_STD_INSPECT/KRAKEN_16_INSPECT/KRAKEN_8_INSPECT(each DB'sinspect.txt),MPA_PKL(the MetaPhlAn.pkl) andFOCUS_DB(holdingacc_taxid_strain.tsv,taxid_lineage.tsvanddb/). If you did notsource config.sh, the script falls back to obvious/path/to/...placeholders and fails loudly. -
Sample and classify a pool of type-material genomes (deterministic via
--seed):conda activate rapdtool_bench python3 scripts/check_representation.py --sample 1500 --seed 42 -o data/bench_out_repcheck.tsv
It prints per-category tallies (
reference,conflictive-both,conflictive-mpa,conflictive-kraken), then suggests 10 reference + 10 conflictive genomes with a ready-to-paste GCA path list;data/bench_out_repcheck.tsvholds the full per-genome table (columns: gca, taxid, phylum, genus, species, kraken_full/16/8, metaphlan, verdict). -
Pick your 20. Either take the suggested list as-is, or curate — this kit used 5 clinical + 5 phylum-diversifiers (all
reference, i.e. present in every DB) and 10conflictive-both(absent from Kraken and MetaPhlAn), one per phylum. To add a specific canonical species not caught by the random sample, check it directly:python3 scripts/check_representation.py GCA_001281725.1 GCA_002902205.1 # E. coli, S. aureus # -> confirms verdict=reference (1/1/1, Y) before you add it
Options: positional GCAs / paths,
-l FILE,--n-ref,--n-conf,--sample,--seed. -
Write
data/mock_genomes.list— one genome path per line (inline#comments and blank lines are ignored by bothcheck_representation.py -landmake_mock.sh -l). The verdicts and full rationale for the shipped list are inmock_design.md.
Tip: dry-run the light path first — the census (Step 0b) and the figure/number verification need only
$TAXONKIT_DB, no large database download. Confirm those work before committing disk and time to the ~104 GB Kraken2 index for a full re-run.
Harmonises every profile to a single taxonomy so OPAL matches taxa consistently and the updated phylum names from FOCUS resolve, using the same dump the FOCUS DB was built from (already on disk — no download). Pick one backend:
(a) taxonkit — recommended, no install/download:
export TAXONKIT_DB=$TAXONKIT_DB # profile2cami.py auto-selects taxonkit(b) ete3 — build a pinned sqlite once (pip install ete3):
# run from the repository base directory
python3 scripts/setup_taxdb.py --url $TAXONKIT_DB/../taxdump.tar.gz \
-o data/taxdb/taxa.sqlite
export PROFILE2CAMI_TAXDB=$PWD/data/taxdb/taxa.sqliteWrites taxdb.version (source + SHA-256 + phylum-resolution check) — keep it with
the analysis. On another machine, pass a dated archive URL instead:
https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump_archive/taxdmp_<YYYY-MM-DD>.zip.
Force a backend explicitly with profile2cami.py --backend {taxonkit,ete3}.
The strongest result in the study and the cheapest to reproduce. It measures what each database contains, independently of any simulated community:
source config.sh # the script reads the DB paths from the environment
conda activate rapdtool_bench # (or put $BENCH_ENV_BIN first on PATH)
scripts/check_representation.py --sample 30209 --seed 42 -o data/census_full.tsv # ~30 s
python3 scripts/plot_census.py \
-i data/census_full.tsv -l data/mock_genomes.list -o figures/censusProduces data/census_full.tsv (one row per genome: gca, taxid, phylum, genus, species,
kraken_full/16/8, metaphlan, verdict) and the figure in PNG + SVG + PDF.
Result on this system: of 30,209 type-material genomes, 55.8 % are absent from the Kraken2 standard database (103.7 GB), 17.3 % from MetaPhlAn4, and 11.3 % (3,423) from both. Capping Kraken2 to 8.1 GB changes the absence rate by <0.01 pp.
This step also defines the population the mock genomes are sampled from, which is what makes the selection defensible rather than cherry-picked. See
benchmark_rationale.md§2 andmock_design.md.
Pass the inputs/DBs as environment variables (they override the CONFIG defaults, so
you don't edit the script). First a REPEATS=1 smoke test to validate the
plumbing and that Kraken-full fits in RAM, then re-run with REPEATS=3 for the reported
medians — same command, only REPEATS changes:
With the conda env inactive — or on any host where
PATHresolution depends on the current directory — every tool exits 126/127 in 0.0 s, and the whole matrix "completes" in minutes with no data.
BENCH_ENV_BINpoints at the env'sbin/and setskraken2,brackenandmetaphlanat once;RAPDTOOLpoints at the wrapper. With these,conda activateis not needed. A preflight check now aborts in seconds if anything is missing — you should see[preflight] all enabled tools and inputs presentbefore the first tool starts.
M=$MOCK_ROOT/mock_ln_30M # one dataset; full matrix below
BENCH_ENV_BIN=$BENCH_ENV_BIN \
RAPDTOOL=$RAPDTOOL \
THREADS=16 REPEATS=1 OUTDIR=$PWD/results/bench_ln_30M \
ASSEMBLY=$M/asm/final.contigs.fasta \
READS_R1=$M/reads_R1.fastq \
READS_R2=$M/reads_R2.fastq \
KRAKEN_DB_FULL=$KRAKEN_DB_FULL \
KRAKEN_DB_CAP16=$KRAKEN_DB_CAP16 \
KRAKEN_DB_CAP8=$KRAKEN_DB_CAP8 \
MPA_DB=$MPA_DB \
scripts/run_benchmark.shIndividual launchers can also be set one by one (KRAKEN2=, BRACKEN=,
METAPHLAN=) if the tools live in different places.
Give each dataset its own OUTDIR — run_benchmark.sh appends to
summary.csv, so a shared output directory silently mixes datasets into one file.
The clone ships each dataset's
summary.csvwith the study's own rows, and the append above is unconditional: the header is written only when the file is absent. PointingOUTDIRat a shipped directory therefore adds the new rows underneath the committed ones, and the medians printed at the end are computed over both runs at once — with nothing in the file marking which rows came from which. Delete the directory first:rm -rf results/bench_ln_30M # re-run rebuilds it, header included
git checkout -- results/restores the committed copies afterwards. Onlysummary.csvbehaves this way:db_sizes.csvis truncated at the start of each run and the RaPDTool output directories are overwritten (--force), so both are safe to re-run over.
To run the whole matrix (~6 h; MetaPhlAn scales linearly with read count):
# run from the repository base directory
for D in ln_3M ln_10M ln_30M equalcov_30M; do
M=$MOCK_ROOT/mock_$D
BENCH_ENV_BIN=$BENCH_ENV_BIN \
RAPDTOOL=$RAPDTOOL \
THREADS=16 REPEATS=1 OUTDIR=$PWD/results/bench_$D \
ASSEMBLY=$M/asm/final.contigs.fasta \
READS_R1=$M/reads_R1.fastq READS_R2=$M/reads_R2.fastq \
KRAKEN_DB_FULL=$KRAKEN_DB_FULL \
KRAKEN_DB_CAP16=$KRAKEN_DB_CAP16 \
KRAKEN_DB_CAP8=$KRAKEN_DB_CAP8 \
MPA_DB=$MPA_DB \
scripts/run_benchmark.sh
doneSanity check while it runs: the first summary.csv row must show a wall time
well above 0.0 s. A row at 0.0 s with exit_code 126 or 127 means the tool never
started — stop immediately rather than letting the night pass.
Re-running after a partial failure: prefer deleting the affected bench_*
directories and starting that dataset again, over editing summary.csv by hand. The
outputs of one directory should always come from one command with one configuration —
hand-pruned rows leave orphan .stderr/.time.txt files that are impossible to
interpret later. The mock datasets under $MOCK_ROOT/mock_* are expensive and validated;
never delete those to fix a benchmark problem.
REPEATS=1 is correct for accuracy; all compared tools are deterministic, so
replicating accuracy measures nothing. REPEATS=3 is used on one dataset for the
resource medians, and the reported medians name the dataset — and therefore the depth —
they came from.
Individual tools can be switched off — every toggle is environment-overridable:
RUN_RAPDTOOL_FULL, RUN_RAPDTOOL_SCREEN, RUN_KRAKEN_FULL, RUN_KRAKEN_CAP16,
RUN_KRAKEN_CAP8, RUN_METAPHLAN, RUN_MOTUS. e.g. re-run only RaPDTool:
RUN_KRAKEN_FULL=0 RUN_KRAKEN_CAP16=0 RUN_KRAKEN_CAP8=0 RUN_METAPHLAN=0 ... scripts/run_benchmark.shRaPDTool launcher. RaPDTool ships as an Apptainer wrapper installed in its own conda env (
rapdtool). The benchmark itself runs fromrapdtool_bench(which haskraken2/metaphlan/… and also/usr/bin/apptainer). Passing the absolute wrapper path asRAPDTOOL=$RAPDTOOL(above) runs it correctly fromrapdtool_benchand lets/usr/bin/timemeasure RaPDTool's real peak RSS (the wrapperexecs apptainer). If that ever fails, fall back to running it in its own env withRAPDTOOL_ENV=rapdtool(drop theRAPDTOOL=…line) — but noteconda runadds a persistent python parent, so peak RSS for RaPDTool may be under-reported.
Check that summary.csv in the OUTDIR has exit_code=0 on every row before trusting
any result. Produces, under the OUTDIR:
summary.csv— per-replicate wall-clock + peak RSS for each tooldb_sizes.csv— on-disk database sizes- median wall-time / RAM printed to the terminal
- each tool's outputs (
rapdtool.repN/…,*.bracken,*.report,*.profile)
Also record for RaPDTool the DB sizes from rapdtool --where (mash + FOCUS) into
db_sizes.csv.
profile2cami.py auto-detects the format; it resolves each species leaf to an NCBI
taxid and derives the full standard-rank lineage from the pinned taxonomy, so all
tools land on the same tree.
Pass
-s mockto every conversion, including the gold standard. OPAL matches a profile to the gold standard by sample ID; any profile whose ID differs is silently skipped with a warning, and if all of them differ OPAL reportsNo profile could be evaluated. Do not name the samples after the tools — the tool name is carried by-lat the OPAL step, not by the sample ID.
export TAXONKIT_DB=$TAXONKIT_DB # Step 0 backend (see above)
B=results/bench_ln_30M # the OUTDIR from Step 1
mkdir -p $B/profiles
# RaPDTool full (assembly) and screen (reads, matched input vs the classifiers):
python3 scripts/profile2cami.py $B/rapdtool.rep1/profilesfmbm/*/output_All_levels.csv \
-f focus -s mock -o $B/profiles/rapdtool_full.profile
python3 scripts/profile2cami.py $B/rapdtool_screen.rep1/profilesfmbm/*/output_All_levels.csv \
-f focus -s mock -o $B/profiles/rapdtool_screen.profile
for k in full cap16 cap8; do
python3 scripts/profile2cami.py $B/kraken2_${k}.rep1.bracken -f bracken -s mock \
-o $B/profiles/kraken2_${k}.profile
done
python3 scripts/profile2cami.py $B/metaphlan.rep1.profile -f metaphlan -s mock \
-o $B/profiles/metaphlan.profileCheck the abundance mapped=…% line on stderr for each; investigate any
unresolved: names if coverage < 90 %. All six should report 100 %.
RaPDTool applies a 1 % abundance cutoff in
rapdtool_confidence.tbl/.txt. Feed OPAL the raw profile —profilesfmbm/*/output_All_levels.csv(207 species onmock_ln_30M) — not the confidence table (16 species).Filtering RaPDTool at 1 % while Bracken stays unfiltered would manufacture the precision result: 8,129 species vs 16 compares a filtered output against an unfiltered one, which measures nothing.
What is reported instead is a threshold sweep applied to every tool alike — none / 0.1 % / 1 %. The 1 % row is then RaPDTool's default operating point evaluated fairly, with the same filter applied to the competitors.
Note for the 1 % row: two of the twenty genomes are below 1 % by design (0.89 % and 0.76 %), so maximum recall at that threshold is 18/20 for every tool.
- CAMI datasets: the gold-standard profile ships in CAMI format already — use it directly.
- Your own mock: from the 2-column table
make_mock.shwrote (<taxid|name><TAB>abundance). Same-s mockas every other profile:
python3 scripts/profile2cami.py $MOCK_ROOT/mock_ln_30M/mock_composition.tsv -f truth \
-s mock -o results/bench_ln_30M/gold_standard.profileEach dataset has its own gold standard — the equal-coverage control and the depth series are different communities. Never evaluate a profile against another dataset's gold standard.
mock_composition.tsv example (tab- or comma-separated; abundance scale is
renormalised per rank):
# taxid or name <TAB> relative_abundance
470 60
Staphylococcus aureus 20
Acinetobacter_johnsonii 20
Shortcut — Steps 2 + 4 together for every dataset: run_opal.sh converts all six
profiles + the gold standard (matching SampleID) and runs OPAL, aborting a dataset if
any conversion yields an empty profile:
conda activate rapdtool_bench # profile2cami needs taxonkit
export TAXONKIT_DB=$TAXONKIT_DB
scripts/run_opal.sh ln_3M ln_10M ln_30M equalcov_30MOr run OPAL by hand on an already-converted dataset:
B=results/bench_ln_30M
OPALPY=$OPAL_PY # calling the env's python directly
# avoids conda-activate issues
$OPALPY $OPAL_BIN \
-g $B/gold_standard.profile -o $B/opal \
$B/profiles/rapdtool_screen.profile $B/profiles/rapdtool_full.profile \
$B/profiles/kraken2_full.profile $B/profiles/kraken2_cap16.profile \
$B/profiles/kraken2_cap8.profile $B/profiles/metaphlan.profile \
-l "RaPDTool_screen,RaPDTool_full,Kraken2_full,Kraken2_16GB,Kraken2_8GB,MetaPhlAn4"Gives per-rank recall (Completeness), precision (Purity), F1, L1 norm, Bray–Curtis,
weighted UniFrac in $B/opal/results.tsv plus an HTML report. Reporting and
interpretation are at genus/species, the ranks RaPDTool targets. What each metric
means, and how to avoid misreading it, is in benchmark_rationale.md §3.
OPAL gives per-rank recall/precision/F1, L1 and Bray–Curtis. For RaPDTool, use OPAL for ABUNDANCE only (L1, Bray–Curtis). Its OPAL recall/precision reflect the FOCUS profile's false-positive tail and understate detection — detection comes from mash (Step 4b). The competitors' OPAL detection metrics are valid (their profiles are their detection).
RaPDTool has two species outputs. DETECTION is the mash-screen confidence table (
rapdtool_confidence.tbl), its confident species calls — the analogue of MetaPhlAn's marker-filtered list. ABUNDANCE is the FOCUS profile (Step 4a), which the tool itself flags "cautious at species level". Scoring detection from FOCUS understates RaPDTool badly (FOCUS ~204 species / ~184 false positives; mash: the true species, 0 FP). So detection = mash, abundance = FOCUS.
export TAXONKIT_DB=$TAXONKIT_DB
# recall / precision / F1 + reference-vs-conflictive split, from the mash table:
scripts/mash_detection.py --bench results/bench_ln_30M --gold results/bench_ln_30M/gold_standard.profile \
--split data/mock_genomes.list
# real data (credit a reclassified member as its gold taxon, e.g. B. spizizenii -> subtilis):
scripts/mash_detection.py --bench results/bench_zymo_even --gold results/bench_zymo_even/gold_dna.profile \
--extra-true 96241:1423On the mocks this gives recall 1.0, precision 1.0, F1 1.0, 0 false positives (0.97 at 3 M), and the split 10/10 reference + 10/10 conflictive — the central experiment. RaPDTool detects the conflictive species that the competitors (0/10, absent from their databases) cannot. Never present the conflictive result without the census (Step 0b).
Abundance-threshold analysis (context for the FOCUS profile, not detection):
B=results/bench_ln_30M # define it here too — this block may be run in a fresh shell
scripts/threshold_sweep.py --truth $B/gold_standard.profile -o figures/threshold_sweep
scripts/plot_f1_threshold.py # -> figures/f1_threshold.*
scripts/plot_opal_depth.py # -> figures/opal_depth.* (after all 4 datasets)The FOCUS abundance profile carries a low-abundance tail; under an abundance cutoff applied uniformly to every tool's output it overtakes MetaPhlAn on F1 (1.0 at 0.5 %). This is a caveat for interpreting FOCUS composition — RaPDTool's detection F1 is already 1.0 from the mash table, no threshold. The cutoff is a post-hoc output filter, distinct from each tool's internal detection; full rationale in
benchmark_rationale.md§3 and Phase 5.
Which mode is the fair comparison?
rapdtool_screenconsumes the same reads as Kraken/MetaPhlAn — apples-to-apples, and it is where the mash confidence table lives.rapdtool_fullis assembly-based and reserved for genome recovery (Step 5); the read classifiers cannot consume the assembly.
Read classifiers produce no bins; this axis is a capability, not a contest.
Bin completeness/redundancy come from the RaPDTool output (miComplete, in
results/bench_<dataset>/rapdtool.rep1/workfmbm/outmicomplete/miCompleteOut_*.tab);
cross-check with CheckM if desired. Bins are in .../rapdtool.rep1/species_bins/.
Summarise and plot completeness vs contamination across datasets:
scripts/plot_micomplete.py # -> figures/micomplete.{svg,png,pdf} (reads results/bench_ln_30M + equalcov)The two chimeric bins (Fusobacterium massiliense, Corallococcus praedator) are correctly flagged by miComplete's redundancy, and are reported as such (Table 5).
⚠ AMBER is not runnable on this mock. It needs
gold_standard_binning.tsv, a contig→genome truth table, andmake_mock.shproduces only a composition gold standard (mock_composition.tsv). So this step measures bin quality, not bin correctness, and no binning-accuracy claim is drawn from it. To close the gap: either use a dataset that ships a binning gold standard (CAMI II), or extendmake_mock.shto track each simulated read's source genome and derive the contig→genome truth from the assembly (~a day's work, keeps everything in-house).The gap is bounded by what the binning step is for. RaPDTool bins with MetaBAT2 + Binning_refiner — an established, well-benchmarked combination — and claims no advance in binning itself; the step is there to recover genomes good enough to be associated with their nearest type strain, fast and within laptop-class memory. Step 5d measures that against MetaWRAP on the identical assembly.
Measures whether the rank RaPDTool resolves an organism to tracks its genomic
distance from the database — species only when genuinely close, genus at moderate
distance, abstention when far. Graceful degradation is the safe, desirable behaviour;
a confident species call for a distant genome would be the failure. See
benchmark_rationale.md §4b for the rationale.
export TAXONKIT_DB=$TAXONKIT_DB
# 1. distance-stratified selection: sample across novelty tiers, download, and MEASURE
# each genome's minimum Mash distance to RaPDTool's database (the x-axis).
scripts/make_mirror_distance.py --per-tier 6 --seed 42 -o data/mirror_distance.tsv
# -> then hand-pick ~2-3 per distance band into data/mirror_dist_genomes.list, spanning
# 100 % (positive controls, species IS in DB) down to ~70 % identity. The shipped
# list already does this for 14 genomes.
# 2. equal-coverage vector (non-detection must be distance, not coverage)
scripts/make_abundance.py -l data/mirror_dist_genomes.list -o data/mirror_dist_abundance.txt \
--equal-coverage --depths 15
# 3. build the mock. NOTE --acc-map: these genomes are NOT in focus_build, so the
# default accession->taxid table cannot resolve them and the gold standard would
# come out empty. The acc-map is taxid-per-genome, built alongside the list.
scripts/make_mock.sh -o $MOCK_ROOT/mock_mirrordist_15M -t 16 -n 15000000 \
-b data/mirror_dist_abundance.txt --seed 42 -l data/mirror_dist_genomes.list \
--acc-map data/acc_taxid_mirrordist.tsv
# 4. run RaPDTool full+screen, with Kraken2 as positive control (all are in its DB)
M=$MOCK_ROOT/mock_mirrordist_15M
BENCH_ENV_BIN=$BENCH_ENV_BIN \
RAPDTOOL=$RAPDTOOL \
THREADS=16 REPEATS=1 OUTDIR=$PWD/results/bench_mirrordist \
RUN_KRAKEN_CAP16=0 RUN_KRAKEN_CAP8=0 RUN_METAPHLAN=0 \
ASSEMBLY=$M/asm/final.contigs.fasta \
READS_R1=$M/reads_R1.fastq READS_R2=$M/reads_R2.fastq \
KRAKEN_DB_FULL=$KRAKEN_DB_FULL \
scripts/run_benchmark.sh
# 5. crosswalk resolved rank against measured distance, and plot
scripts/plot_mirror_distance.py # -> figures/mirror_distance.{svg,png,pdf}What to read is the resolved rank vs distance, not an accuracy score. For each input
genome (with its measured Mash identity), the finest rank RaPDTool resolved is read from
results/bench_mirrordist/rapdtool.rep1/rapdtool_confidence.tbl — the full-mode
table, which is the path whose cutoffs are fixed and documented. rapdtool_results.pl
sorts each bin by its Mash distance to the nearest reference:
| Block in the table | Mash distance | Identity | Rank |
|---|---|---|---|
Species with high confidence |
< 0.05 | > 95 % | species |
Genus with high confidence |
0.05 – 0.08 | 92 – 95 % | genus |
FOCUS profile only |
≥ 0.08 | < 92 % | genus, if FOCUS places it |
| absent from all three | — | — | no call |
Why not screen mode.
screenapplies a single--screen-identitycutoff (0.95) with no genus tier, so classifying this experiment fromscreenreports every genus call as FOCUS-derived and leaves the tool's own genus threshold untested. Both modes run here and both tables ship;plot_mirror_distance.pyreads thefullone. The x axis is independent of either — it is the measured minimum Mash distance to the database fromdata/mirror_distance.tsv.
Result on the shipped 14-genome set (all behaved correctly):
| Mash identity to nearest DB genome | rank resolved | resolved by | n |
|---|---|---|---|
| 100 % (in DB, positive control) | species | Mash, species tier | 2 |
| 97–99 % | species (nearest congener) | Mash, species tier | 3 |
| 92–95 % | genus only — Mash stops calling species at 95 % | Mash, genus tier | 3 |
| 82–91 % | genus — beyond Mash's 0.08 cutoff | FOCUS profile only | 3 |
| 70–76 % | abstains — not reported | — | 3 |
No genome below the 95 % threshold received a species call, and nothing below ~80 % identity was reported at all — i.e. graceful degradation, a safety property, not silent misassignment. The result is given as "below ~80 % identity RaPDTool abstains" rather than as a precise cutoff: Mash distance saturates near 70–75 % identity, so the abstained genomes' exact distances are not meaningful.
Real Illumina data for a third-party-defined community; the only dataset here not
simulated from genomes we chose. Full provenance, composition table and the two
disclosure requirements are in $ZYMO_DIR/README.md — read it before
building the gold standard, particularly the sequence-abundance vs cell-abundance
distinction, which biases the comparison if ignored.
cd $ZYMO_DIR && md5sum -c md5sums.txt # always verify first
# even community (D6300); repeat with ERR2935805 for the log community
Z=$ZYMO_DIR
BENCH_ENV_BIN=$BENCH_ENV_BIN \
RAPDTOOL=$RAPDTOOL \
THREADS=16 REPEATS=1 OUTDIR=$PWD/results/bench_zymo_even \
READS_R1=$Z/ERR2984773_1.fastq.gz READS_R2=$Z/ERR2984773_2.fastq.gz \
KRAKEN_DB_FULL=$KRAKEN_DB_FULL \
KRAKEN_DB_CAP16=$KRAKEN_DB_CAP16 \
KRAKEN_DB_CAP8=$KRAKEN_DB_CAP8 \
MPA_DB=$MPA_DB \
RUN_RAPDTOOL_FULL=0 \
scripts/run_benchmark.shRaPDTool
fullis disabled above because no assembly exists for these reads yet. To include it, assemble first (megahit -1 … -2 … -o asm) and passASSEMBLY=asm/final.contigs.fasta— remembering that FOCUS needs.fasta, not MEGAHIT's.fa.
Gold standards are already built: zymo_composition_dna.tsv (for Kraken2/Bracken) and
zymo_composition_genomecopy.tsv (for MetaPhlAn). Convert whichever applies with
profile2cami.py -f truth -s mock.
Shows a dedicated ensemble binner does not beat RaPDTool on recovery (rationale §4d). MetaWRAP is given the same assembly RaPDTool consumed, so only binning/refinement differs; the script then re-scores both bin sets with the same evaluator (miComplete/Bact105, inside RaPDTool's SIF) so the numbers are comparable. Containerised — nothing to compile.
# one-time setup (paths overridable via env; see config.sh: METAWRAP_SIF, CHECKM_DB, RAPDTOOL_SIF)
apptainer pull "$METAWRAP_SIF" docker://quay.io/biocontainers/metawrap-mg:1.3.0--hdfd78af_1
# CheckM DB -> $CHECKM_DB (binning + bin_refinement, 1.4 GB):
# wget .../CheckM_databases/checkm_data_2015_01_16.tar.gz && tar -xzf ... -C "$CHECKM_DB"
# one script per dataset: binning + bin_refinement + miComplete re-scoring, all containerised
METAWRAP_SIF=$METAWRAP_SIF CHECKM_DB=$CHECKM_DB RAPDTOOL_SIF=$RAPDTOOL_SIF \
scripts/run_metawrap.sh ln_30M $MOCK_ROOT/mock_ln_30M
METAWRAP_SIF=$METAWRAP_SIF CHECKM_DB=$CHECKM_DB RAPDTOOL_SIF=$RAPDTOOL_SIF \
scripts/run_metawrap.sh zymo $ZYMO_DIR/asm_dir # a dir with asm/final.contigs.fasta + reads_R{1,2}.fastq
# -> results/metawrap/<ds>.checkm.stats (MetaWRAP's native CheckM numbers)
# results/metawrap/<ds>.micomplete.tab (common-evaluator re-scoring; the comparable numbers)
# results/metawrap/summary.csv (wall time, peak RSS per step)Only
binning/bin_refinementare run (CheckM DB, 1.4 GB). MetaWRAP's read taxonomy module is Kraken2 (§4c gap applies by identity); naming its bins would needclassify_bins(NCBI_nt, +71 GB) — not run, but the footprint is reported in §4d.
The medians (Step 1), OPAL metrics (Step 4), miComplete + bin counts (Step 5), the MetaWRAP MAG comparison (Step 5d) and DB sizes are what fill the manuscript results tables (Tables 1–5). The genome-recovery / type-material rows (genomes recovered, completeness/redundancy, type-material placement, novel-taxon resolution, per-species FASTA) are the columns only RaPDTool fills — the core argument.
-
taxdb.version(pinned dump source + SHA-256) -
data/census_full.tsv+ the census figure -
data/mock_genomes.list,data/mock_abundance.txt,data/mock_abundance_equalcov.txt(the fixed vectors — without these the mocks are not reproducible) -
summary.csv+db_sizes.csvper dataset (bench_<dataset>/) - all
*.profileCAMI files + each dataset'smock_composition.tsv - tool versions and exact command lines
- hardware spec (CPU, cores, RAM) and
THREADS - OPAL output directory per dataset (AMBER not applicable — see Step 5)
- which dataset the reported resource medians came from, and at what depth
| Hardware | Intel Core i9-14900, 24 physical cores / 32 threads, 125 GB RAM, NVMe |
| Threads | 16 |
| RaPDTool | v2.3.0 (Apptainer SIF; bundles FOCUS, MetaBAT2, Binning_refiner, miComplete/Bact105, Mash, KronaTools) |
| Kraken2 | 2.17.1 · DBs: standard / 16 GB / 8 GB, build 2026-06-26 |
| Bracken | 3.0.1 |
| MetaPhlAn | 4.2.5 · DB vJan26_CHOCOPhlAnSGB |
| MEGAHIT | 1.2.9 |
| InSilicoSeq | 2.0.1 |
| OPAL | 1.0.14 |
| MetaWRAP | 1.3.0 (biocontainer metawrap-mg:1.3.0--hdfd78af_1; bundles CheckM, DB checkm_data_2015_01_16) |
| Taxonomy | NCBI taxdump 2026-07-10, sha256 c1b91199… (the dump the FOCUS DB was built from) |
The boundaries of the design are set out in benchmark_rationale.md §4. The one that
governs how the outputs here are read: the mock communities carry no contig→genome truth
table, so AMBER is not runnable and no binning-accuracy metric is reported — bin
quality is measured (miComplete, completeness and redundancy), bin taxonomic
correctness is not claimed. That boundary follows the tool's purpose: binning is
performed by MetaBAT2 + Binning_refiner and is the step that makes a genome available
for placement against its nearest type strain — the placement, not the binning
algorithm, is what is claimed here — and Step 5d measures the trade-off against a
dedicated MAG pipeline. Independent validation is provided by the ZymoBIOMICS even
community (Step 4b), a third-party standard with published composition that is not part
of RaPDTool's curated set.
Dual-licensed by artifact type:
- Code — everything under
scripts/— is under the MIT License (LICENSE). - Data, result tables and figures —
data/,results/,figures/— are under CC-BY-4.0 (LICENSE-CC-BY-4.0.md).
If you use this kit, please cite the accompanying publication and this repository (archived at Zenodo, DOI: 10.5281/zenodo.21528297):
The RaPDTool authors. RaPDTool: type-material–anchored, genome-resolved metagenomics on a laptop. Bioinformatics (under review). Benchmark kit: https://github.com/BioTools-Dev/rapdtool-benchmark