Repository navigation
Description
Origin
#303 (wetSpring) prompted this. It is a GPU denoiser that adds every significant sequence as a new center in one pass. We aren't pursuing that design: it skips alignment, re-learns errors inside each sample, and seems to borrows from several methods without publishing results checking agreement with them. Its starting idea, denoising without DADA2's iterative splitting, has a cleaner published form in UNOISE3 (Edgar 2016), and that's what this issue tests.
Background
UNOISE3 makes one pass over the unique sequences, most abundant first. Each sequence becomes a new center unless it is close to an existing, more abundant center and its abundance is low enough relative to that center. "Low enough" is a fixed rule, a maximum abundance ratio β(d) = 1/2^(αd+1), tuned on Illumina data and with no per-run error model (UNOISE2 preprint, which also reports results on ITS).
The author notes that UNOISE "was designed and tested on Illumina reads" and does less well on 454 and Ion Torrent data. The main problem is indel errors from miscalled homopolymer lengths, which "cause two issues: incorrect alignments and inflated abundance of incorrect reads" (USEARCH FAQ). PacBio HiFi data has the same kind of error, albeit at much lower rates.
Deblur has a different limit: it assumes reads of one fixed length. Tedersoo et al. 2022 (doi:10.1111/mec.16460) state that it can't be used for ITS reads because their length varies. That statement is quoted in a QIIME 2 forum thread on fungal ITS workflows.
DADA2 swaps the fixed rule for a learned error model and a Poisson abundance test. It then splits clusters one new center at a time, re-assigning members and recomputing p-values after each. That splitting loop is a large part of the cost, and it can't be run in parallel within a sample.
Question
Does a single pass in order of abundance, with DADA2's learned error model and abundance p-value replacing UNOISE3's fixed ratio, match dada's accuracy across technologies compared to DADA2's iterative approach? And what does it save?
Arms
All arms use the same derep input, learn-errors output, global NW alignment, k-mer screen, and OMEGA_A.
dada(current): splits one new center at a time.- Single pass, error model: process uniques in decreasing order of abundance. For each one, compute lambda against each existing center that passes the screen. Take the best (largest-lambda) center and apply DADA2's p-value test. If p < OMEGA_A, the sequence becomes a new center; otherwise it joins that center. No re-assignment afterwards.
- Arm 2 plus one re-assignment pass: after the pass, move each member to its most likely center once. This separates what the error model contributes from what DADA2's re-assignment contributes.
- Single pass, UNOISE3 rule: the same loop as arm 2, but with the β(d) ratio rule instead of the error model. This is the control for the error model. Compare it with
vsearch --cluster_unoiseas a check that our loop reproduces UNOISE3.
Add arms 2–4 as an experimental switch on dada, not a new subcommand.
Data
Each dataset tests a different way the fixed rule could fail:
- Illumina MiSeq 16S V4: UNOISE3's home turf; all arms should agree here.
- Illumina NovaSeq/NextSeq with binned qualities: a newer chemistry with a different error profile.
- PacBio HiFi full-length 16S (MSA-1003 truth set): long reads, homopolymer indels, and a different error profile.
- ITS2: reads of varying length with more variation between real sequences.
Measures
- Accuracy against a known mock community with
reference-eval, wherever a truth set exists. - ASV set identity against
dadaon the same input: shared, gained, and lost ASVs, with the abundance moved. Don't use ASV counts alone; they hide churn. - Wall time, alignments performed, and how well the work parallelises. A single pass compares each unique with every existing center, roughly the same number of alignments as
dada, so any savings will come from dropping the splitting loop, not from fewer alignments.
Predictions, stated so they can be disproved
- On MiSeq V4, arms 2–4 come close to
dada. - Arm 4 drifts from
dadaand from the truth on PacBio and binned-quality data, while arm 2 does not. That would confirm the error model, not the single pass, is what makes the method work across technologies. - Arm 2's differences from
dadaare concentrated in low-abundance sequences that sit between two centers, and arm 3 removes most of them.
If arm 2 holds up, it could be a cheaper mode for very large or pooled runs (see #106), and its comparisons could be batched for #17. GPU work is out of scope here.
Activity
cjfields commentedon Oct 31, 2026
Plan
Approach: a switchable backend
run_dada(src/dada.rs) returns the cluster structureB, and everything downstream reads only that: JSON output,--failed-uniques, chimera removal,reference-eval. A single-pass backend that builds the sameBgets all of that unchanged. It also works indada-pooledwithout extra effort, since that mode calls the same function.Single pass reuses DADA2's comparison step instead of adding a new one:
b_compare_parallelfrom that center against all the uniques, exactly asdadadoes after each split. The existing greedy option already skips uniques more abundant than the center.b_shuffle_converge), the search for the next sequence to split off (b_bud_incremental), and the p-value refresh after each split (b_p_update) all drop out.The k-mer screen, alignment, lambda and p-value code stay identical to
dada, which is what makes the arms comparable.Switches
Two independent switches, following
--align-backendand--screen-backend. Both default to today's behaviour, are written to the output JSON only when not the default, and are marked experimental:--partition divisive|single-pass:divisiveis today'sdadaand stays the default.--birth-rule pvalue|unoise: the test that decides whether a unique becomes a new center.pvalueis DADA2's Poisson test with the learned error model.unoiseis UNOISE3's fixed ratio rule β(d), with d the mismatch count from the stored alignment. It's only valid withsingle-pass.learn-errorsstays ondivisiveand doesn't get the flag, so every arm uses the same error model.Phases
--metrics-jsonalready times the shuffle, split search and p-value refresh separately (shuffle,bud,p_update). The comparisons are unchanged in single pass, so those three timings are the most it can save. If they're small, this issue is purely about accuracy.vsearch --cluster_unoiseon MiSeq V4 before it's used as a control.reference-evaland ASV set identity. Thedev/compare_arms.pyharness designed for Re-assess the LOESS surface default at scale, and make the benchmark harness error-model-aware #205 does the same job.docs/findings/.Decisions to settle before phase 2
dada-pseudowith priors. Out of scope for the first pass.cjfields commentedon Nov 1, 2026
Phase 0 result: the most single pass could save
Cluster runs, pooled, with
--metrics-json(branchsingle-pass-323, #324). "Removable" is shuffle + bud + p_update, the three phases single pass drops. Its share is of the summed phases.Conclusion: speed doesn't justify single pass. #323 continues as an accuracy question: does a single pass with DADA2's error model match
dadaacross technologies, and how does it compare with UNOISE3's fixed rule? Any work on speed is better aimed at the shuffle and p-value refresh within the current algorithm.Side result: on pooled PacBio, k=7 compares 6–7× faster than k=5 (3482 → 487 s at 24 threads, 1013 → 172 s at 96).
run_pacbio.shstill defaults toPACBIO_KMER=5.Next: phase 1 (switch and plumbing, no behaviour change), marked highly experimental alongside
wfa2and the indel work.feat: experimental --partition and --birth-rule switches, no behaviou…
feat: experimental single-pass partition, p-value birth rule (#323)
feat: greedy locks in the single-pass partition (#323)
cjfields commentedon Nov 1, 2026
Phase 2: single pass with the p-value rule, plus greedy locks
On
single-pass-323(#324): 0f65157 (single pass) and 0b0f60c (greedy locks). Uses the choices settled above: expected reads are λ × the centre's own reads, the best centre is the one with the most expected reads, and the MIN_FOLD, MIN_HAMMING, MIN_ABUNDANCE and singleton gates apply. Greedy locks are applied to each newly born centre asdadadoes, for consistency withdada. Nothing is reassigned after the pass.First look: MiSeq SOP, forward reads, 20 samples, 8 threads (local)
ASV sets, single pass (with locks) against divisive:
divisive) output remains byte-identical tomain.This is one small run that only checks single pass works. Accuracy will be judged against truth sets in phase 5.
Next: phase 3 (an optional reassignment pass after single pass) and phase 4 (the UNOISE3 rule as a control).
feat: --reassign, one DADA2 shuffle after the single pass (#323)
cjfields commentedon Nov 1, 2026
Plan update: an indel axis, and the UNOISE rule's definition
Why
UNOISE counts gaps by definition. The UNOISE2 preprint (Edgar 2016, doi:10.1101/081257) says: "Let d be the Levenshtein distance (number of differences including both substitutions and gaps) between M and C". DADA2's defaults count substitutions only, and our indel-aware λ (#291) is opt-in. Comparing UNOISE only against substitution-only arms would mix two differences, the birth rule and whether indels count. So the indel model becomes an axis of its own.
--indel-rates)dada--partition single-pass--reassignComparing arm 2 with indels against arm 4 tests the learned error model against the fixed rule, with indels counted in both. The arms with indels off show what substitution-only DADA2 does.
Single pass already honours
--indel-rateswith no changes. On the MiSeq SOP (forward reads, pooled), the indel λ adds 2 and removes 1 ASV under divisive, and adds 2 under single pass, which is expected on Illumina. Learning indel rates leaves the error model unchanged (err_inanderr_outare identical), so these comparisons share one model.The UNOISE rule (arm 4)
--unoise_alphaalso defaults to 2.0). A unique is absorbed by a centre if skew = its reads / the centre's reads ≤ β(d); otherwise it becomes a new centre.--iddef 2default.--min-abund, set the same for every arm. UNOISE2 used γ = 4, and vsearch--cluster_unoisedefaults to--minsize 8.Counting differences to keep in mind:
hamming, while Levenshtein counts gap columns, so a 3-base deletion is 1 event but d = 3.--indel-end-zone(10) bases of the 3′ end, whereas arm 4 excludes end gaps.Each arm keeps its own definition.
Cross-check:
vsearch --cluster_unoise --minsize 8 --unoise_alpha 2against arm 4 with--min-abund 8on MiSeq V4. This will be documented on the command page and in the findings write-up.feat: UNOISE birth rule for the single pass (#323)
dev: phase 5 arm-grid driver and summary under dev/unoise (#323)
feat: UNOISE rule counts every member's reads; vsearch UNOISE3 contro…
dev: CHIMERA_EXTRA for run_arms.sh chimera removal (#323)
dev: run_arms.sh timing without /usr/bin/time; wall and peak columns (#…