Introduce unified nonzero_iter API across matrix types
Replaces nested column-major point lookups with a batched iterator that delegates to format-native traversal strategies. The implementation enforces a single pass per matrix type, using row-major iteration for sparse formats and eager collection for packed/columnar layouts while preserving original slot ordering. Memory allocation is optimized by removing `n_cols`-wide buffers in favor of per-row buffering or lazy iteration. Correctness tests verify iterator output against dense baselines across all supported layouts, and architecture documentation is updated to reflect the new format-agnostic query pattern.
This commit is contained in:
@@ -882,3 +882,54 @@ free to drift apart, and they did. If `fill_sub_matrix` itself is
|
||||
rewritten as "drain `nonzero_iter`, scatter into `out[][]`", there is only
|
||||
one traversal per format left to get right — the bug class doesn't just
|
||||
get fixed once, it stops being possible to reintroduce.
|
||||
|
||||
## Implemented (2026-08-20)
|
||||
|
||||
Built as designed above, with one deviation from the original sketch:
|
||||
`nonzero_iter` ended up `Box<dyn Iterator<...>>`, not a bare `impl
|
||||
Iterator`, because `Columnar`/`Packed`/`Sparse`/`Implicit` are genuinely
|
||||
different concrete types and this method isn't on a trait (kept off
|
||||
`BinaryMatrix` deliberately — that trait is used as `dyn BinaryMatrix` in
|
||||
`tests/sparse.rs`, and RPITIT methods aren't dyn-compatible). One `Box`
|
||||
per `nonzero_iter` call, not per cell — negligible next to what it
|
||||
replaces.
|
||||
|
||||
- `BitSliceView::nonzero_among_sorted` / `IntSliceView::nonzero_among_sorted`
|
||||
(`obicompactvec/src/views.rs`): the vector-level `filter`/`filter_map`
|
||||
primitive, exactly as sketched — no new state machine, `std`'s own.
|
||||
- `PersistentSparseBitMatrix::nonzero_iter` (`bitmatrix/sparse.rs`): native,
|
||||
`std::iter::from_fn` over one buffered row at a time via the existing
|
||||
`for_each_genome_in_row` — no `n_cols`-wide allocation, ever.
|
||||
- `PersistentBitMatrix::nonzero_iter` (`bitmatrix/persistent.rs`): dispatches
|
||||
to the above for `Sparse`; for `Columnar`/`Packed`, loops columns,
|
||||
collects each column's `nonzero_among_sorted` hits via `.extend()` (not
|
||||
`flat_map` — a `flat_map` closure can't lazily return something
|
||||
borrowing its own captured sort permutation across separate calls
|
||||
without either boxing per-column or fighting the borrow checker; eager
|
||||
collection into one `Vec` sidesteps it, at zero cost since
|
||||
`fill_sub_matrix` already fully materialized anyway). `Implicit` trivial.
|
||||
- `PersistentBitMatrix::fill_sub_matrix` and `sub_matrix` rewritten to
|
||||
drain `nonzero_iter` — the dispatch bug is gone because there is now
|
||||
only one traversal per format, not because the old one was patched.
|
||||
`PersistentCompactIntMatrix::nonzero_iter` added the same way (counts
|
||||
not excluded, per the earlier ask) — no native low-effort case, since no
|
||||
sparse count format exists, but on the same primitive, ready for one.
|
||||
- `KmerPartition::query_partition_with` (`obikpartitionner/src/query_layer.rs`):
|
||||
stage 2's column-major `for g { for slot { col_value } }` replaced by one
|
||||
`layer.nonzero_iter(&slot_list)` call per layer, format-agnostic.
|
||||
- Tests: `nonzero_iter_matches_dense`, `nonzero_iter_matches_row`, and —
|
||||
the one that actually targets the dispatch bug rather than each type's
|
||||
own correctness — `enum_wrapper_dispatches_to_native_sparse` (builds
|
||||
`PersistentBitMatrix::Sparse(...)` directly, not through `open`, since
|
||||
`open` only auto-detects `Sparse` from a `presence/` dir layout).
|
||||
`cargo test --workspace`: green, no regressions.
|
||||
|
||||
**Measured**: re-ran the `benchmark/` query branch (100k reads × 2
|
||||
specimens, same setup as the original finding). Correctness still 0
|
||||
mismatches. The dense/sparse performance gap is gone — previously sparse
|
||||
~30-50% slower than dense, reproducibly; now within ~1-3% either way
|
||||
(7.42s dense vs 7.60s sparse for `Escherichia_coli--K-12_MG1655`; 5.25s vs
|
||||
5.30s for `Saccharolobus_islandicus--M.16.4`) — noise-level, not a
|
||||
systematic gap. `pack --sparse`'s claimed query win isn't confirmed
|
||||
outright by this (sparse should arguably now *beat* dense on truly sparse
|
||||
real data, not just tie), but the pathological regression is fixed.
|
||||
|
||||
Reference in New Issue
Block a user