feat(sort): per-dataset minimizer cutoff exponent#1772
Conversation
- Add optional cutoff field to MinimizerIndexConfig for datasets needing wider k-mer retention than the default
|
Does the |
|
@rneher Query hashes above the cutoff are discarded before lookup [src]. I believe the slowdown comes from a different mechanism: the global Needs to be profiled to be sure what causes it. I could not figure out how to do the per-dataset cutoff, so this sets the global to Overall it's still very fast, so runtime perf might not have much practical consideration. The decision point is whether it improves anything or not scientifically. This requires some more relevant data and some testing. |
PR: per-dataset minimizer cutoff exponent
feat/minimizer-cutoff->feat/multirefAdds an optional per-dataset
minimizerIndex.cutoffexponent to pathogen.json, allowing datasets to retain a larger fraction of k-mers in the minimizer index. Builds onfeat/multiref(multi-reference scoring). Test with CVA16 dataset using branchcva16-testing-sample-with-cutoffinnextclade_data.feat/multirefin both nextclade and nextclade_dataRelated
Motivation
The default cutoff
1<<28retains ~6.25% of k-mers. For some diverse pathogens, a wider cutoff (e.g.1<<31, ~50%) might improve discrimination by retaining more k-mers per reference. This PR adds the mechanism; whether any dataset benefits from it is a separate evaluation.What changes
cutoff: Option<i64>toMinimizerIndexConfigin pathogen.json schema, update generated schema filescutoff_from_exponent()to resolve the exponent to a raw threshold, plumb per-dataset cutoffs throughmake_ref_search_index()andrebuild, set globalparams.cutofftomax(all per-dataset cutoffs)Design: global vs per-dataset cutoff
The minimizer index has a single
params.cutoffthat the client uses to hash query k-mers. This must be the max of all per-dataset cutoffs because the client hashes queries once before knowing which dataset matches. Per-dataset cutoff is enforced on the index side: each dataset stores only hashes below its own cutoff. Extra query k-mers generated by the wider global cutoff don't match lower-cutoff datasets (verified: zero hit-count differences).Deviations from earlier findings
Verification
Tests: 11/11 Python tests pass (7 multi-ref + 4 cutoff-specific). Rust unit tests pass (cutoff field parses and round-trips correctly).
CLI: cutoff isolation experiment
Three-way comparison to isolate the effect of per-dataset cutoff from multi-ref. All three variants use the
feat/multirefnextclade binary (which readsexpectedMinimizerHits); only the minimizer index differs.feat/multirefmasterminimizerIndex1<<28feat/multirefcva16-testing-samplecutoff1<<28feat/multirefcva16-testing-sample-with-cutoff"cutoff": 311<<31Setup: build binary and three indexes
Test 1: CVA16 score improvement (36 full-length genomes, ~7400 bp)
Commands
All 36 sequences classified as
enpen/enterovirus/cva16in all three variants.Cutoff 31 (C) scores lower than default cutoff (B): mean -4%, min -7%. The wider cutoff increases both numerator (more k-mer hits) and denominator (
expectedMinimizerHits), but the denominator grows faster because it is computed analytically as(L - k) * cutoff / 2^32while actual hits are reduced by non-ACGT k-mers and hash collisions.Test 2: Regression check (177 mixed-pathogen queries)
Commands
Dataset assignment counts (9 CVA16 fragments, 28 HBV, 12 HIV, 128 SARS-CoV-2):
A vs B: zero assignment changes. Multi-ref alone causes no regressions.
B vs C: one SARS-CoV-2 sequence (IMS-10004-CVDP-E64B5426) moved from
BA.2.86(score 0.978) towuhan-hu-1/orfs(score 0.967). This is a borderline case between two valid SARS-CoV-2 datasets -- not a false positive, but a score-ordering change caused by the denominator shift. Both datasets use the default cutoff (1<<28), so their stored minimizers are identical in B and C; only theexpectedMinimizerHitsdenominator differs becausenMinimizers(the rounded expectation) changed fromround((L-k)*cutoff_28/2^32)toround((L-k)*cutoff_28/2^32)-- the same formula, but the index was rebuilt and the literal storednMinimizersvalue shifted by 1-2 due to floating-point rounding. Hit counts are identical (verified: 0 hit-count differences for same-assignment sequences in B vs C).Index-level comparison
nMinimizersexpectedMinimizerHitslengthparams.cutoff1<<281<<281<<31nMinimizersexpectedMinimizerHitsThe SARS-CoV-2
expectedMinimizerHitsis identical in B and C (both use default cutoff for SC2). The score difference comes from thenMinimizerschange between A and B (478 -> 462 for CVA16 is irrelevant; 1839 -> 1868 for SC2 matters because A usesnMinimizersas denominator while B and C useexpectedMinimizerHits).Runtime
The runtime increase is caused by the global
params.cutoffrising from1<<28to1<<31, forcing the client to hash 8x more k-mers per query for all 105 datasets, not just CVA16.Conclusions
Work items
cutoff: Option<i64>toMinimizerIndexConfigin pathogen.json schemacutoff_from_exponent()with range validation (exponent in[1, 32])make_ref_search_index()andrebuildparams.cutoffto max of all per-dataset cutoffs