Skip to content

Make containment-based selection first-class - #1

Open
DanielSprockett wants to merge 10 commits into
mainfrom
containment-selector-integration
Open

DanielSprockett wants to merge 10 commits into
mainfrom
containment-selector-integration

Conversation

@DanielSprockett

Copy link
Copy Markdown
Contributor

Makes the containment-based prototype selection validated on the rewilded-mouse dataset reachable end-to-end through shipped commands. Previously ContainmentSelector was implemented but unreachable (bespoke interface, absent from the registry, no CLI/pipeline path), and the containment matrix it needs was only produced by an out-of-tree analysis script.

Changes

  • Unify the selector interface. ContainmentSelector is now a BaseSelector subclass returning prototype sample_ids (the same contract every selector follows; align resolves sample_id → FASTA). Registered as containment; constructible uniformly from SELECTOR_REGISTRY. Correlation-distance matrix is precomputed once instead of pairwise np.corrcoef in the greedy loop (~4 ms/sample on 183 samples).
  • refrover.containment + refrover containment CLI. Computes the reads-vs-assemblies containment matrix in-package via sourmash MinHash.contained_by, so containment selection is self-sufficient.
  • Real MetaBAT2 variance. The internal coverage schema now carries {sample}_var; run_coverm requests length mean variance; load_coverage normalizes CoverM's {sample} {Method} columns. MetaBAT2 emits real per-sample variance (was hardcoded 0, which silently degraded binning). Other formatters select mean columns by suffix; the format CLI routes through load_coverage.
  • Tier-1 selector ranking. benchmark.rank_selectors + refrover rank-selectors rank selectors in seconds with no alignment, by variance explained via orthogonal projection (correlated selections don't double-count). Reproduces the analysis on real data (MaxMin/containment lead, greedy_var trails).
  • containment_saturation adaptive-k. Greedy residual-variance saturation (the validated method) ported into adaptive_k.py and exposed via --adaptive-k-method. Reproduces the analysis exactly: median k=4 across 183 samples.

End-to-end

refrover containment --read-sketches reads/ --assembly-sketches asm/ --outdir cont/
refrover select --selector containment --containment-matrix cont/containment_matrix.tsv \
                --manifest samples.tsv --k auto --adaptive-k-method containment_saturation \
                --min-jaccard 0.05 --outdir assignments/
refrover rank-selectors --containment-matrix cont/containment_matrix.tsv --outdir ranking/

Tests

33 → 102 passing; ruff clean. New: test_cli.py, test_coverage.py, test_containment.py, test_benchmark.py, plus additions to selector and adaptive-k tests. CoverM parsing is locked by unit tests since CoverM isn't available in the test env.

Notes

  • opus_suggestions.md (a code-review document) is included for reference.
  • Descoped follow-ups: hybrid coverage merging (documented but unimplemented), CI workflow, and the Tier-2 CheckM2 MAG-yield benchmark (needs external tools + ground truth).

- pyproject.toml: installable package with entry point refrover.cli:main
- src/refrover/selectors/: BaseSelector ABC, RandomSelector, MaxMinSelector,
  KMedoidsSelector (pure-numpy PAM), ArchetypeSelector (archetypes package)
- src/refrover/io.py: manifest read/validate
- tests/: 23 passing unit tests covering correctness, diversity, and edge cases
- .gitignore: excludes data/, *.sig, *.zip, build artifacts
- cli.py: Click entry point with sketch/select/align/coverage/format/run/benchmark subcommands
- sketch.py: sourmash sketch + compare wrappers
- similarity.py: load/filter Jaccard similarity matrices; direct Python API computation
- align.py: BWA-MEM2, BWA, minimap2 wrappers; handles paired/single/long/hybrid reads
- coverage.py: CoverM wrapper for per-contig depth
- pipeline.py: RefRoverPipeline orchestrator
- benchmark.py: stub (requires CheckM2)
- selectors/greedy_var.py: greedy coverage-variance maximization selector
- selectors/containment.py: read-containment-based selector with optional species weighting
- selectors/feedback.py: stub (requires alignment loop)
- formatters/: MetaBAT2, SemiBin2, MaxBin2, CONCOCT, generic TSV with registry dispatch
- 61 tests passing; added coverage_df and tiny_manifest fixtures
compare_selectors.py evaluates four prototype selection strategies on the
183-sample containment matrix using two metrics:
- Naive: sum of selected column variances (biased toward correlated selectors)
- Unique: orthogonal projection of the full 182-column matrix onto the selected
  set (removes double-counting of correlated assemblies)

explore_selectors.R generates five diagnostic plots: variance-explained curves
(both metrics side-by-side), per-sample distribution at k=5, selector agreement
heatmap, adaptive-k distribution, and per-sample saturation curves.

Key algorithm: the saturation curve uses greedy forward selection by residual
variance (Gram-Schmidt incremental QR) rather than MaxMin order, which ensures
a monotonically decreasing gains sequence necessary for threshold-based elbow
detection.
The containment-based selection validated on the rewilded-mouse dataset was
implemented but unreachable: ContainmentSelector used a bespoke interface, was
absent from SELECTOR_REGISTRY, and had no CLI or pipeline path. This makes it a
first-class selector.

Interface unification
- ContainmentSelector now subclasses BaseSelector and returns prototype
  sample_ids, the same contract every selector follows (a prototype is the
  sample whose assembly it is; align resolves sample_id -> FASTA). This removes
  the implicit sample==assembly coupling in favour of the explicit manifest
  mapping that align already uses.
- Accepts min_jaccard= as an alias for min_containment so the registry can
  construct it uniformly. Gains BaseSelector's k/threshold validation.

Performance
- Precompute the candidate correlation-distance matrix once instead of calling
  np.corrcoef pairwise inside the greedy loop. ~4 ms/sample on the 183-sample
  matrix (was O(k*n) corrcoef calls per query).

Wiring
- Registered as "containment" in SELECTOR_REGISTRY; added CONTAINMENT_SELECTORS
  so the CLI/pipeline know to feed it a containment matrix.
- refrover select / refrover run accept --containment-matrix; error clearly when
  the required matrix (or --sketches for Jaccard selectors) is missing.
- RefRoverPipeline accepts containment_matrix= and uses it for selection,
  skipping the sketch+compare step.
- load_containment_matrix() added to similarity.py.

Tests: 9 new (registry membership, BaseSelector conformance, registry
construction, CLI select happy path + both missing-input errors). 78 pass.

Descoped from this branch: the MetaBAT2 variance fix (it is a coverage-schema
change touching load_coverage and all five formatters, not the few lines first
estimated) — to follow as its own change.
The MetaBAT2 formatter wrote every per-sample variance column as 0 because
CoverM was only ever asked for mean depth, and the internal coverage schema had
no place to carry variance. MetaBAT2's jgi format uses that column, so binning
silently lost differential signal.

Coverage schema now carries variance:
  length, {sample}_depth (mean), {sample}_var (variance)

- run_coverm requests `--methods length mean variance` (configurable).
- load_coverage normalizes CoverM's `{sample} {Method}` columns into the schema
  via normalize_coverage: ` Mean` -> _depth, ` Variance` -> _var, ` Length` ->
  a single collapsed `length`. Already-normalized frames pass through unchanged.
- MetaBAT2 formatter emits the real paired variance, falling back to 0.0 only
  when a sample has no variance column (mean-only CoverM runs).
- generic / semibin2 / maxbin2 / concoct select mean columns by suffix via a
  shared formatters/_columns helper, so variance columns never leak into the
  mean-only formats. MaxBin2 .abund files are now named by clean sample id.
- The standalone `refrover format` command routes through load_coverage instead
  of a raw pd.read_csv, so it normalizes raw CoverM tables too.

Tests: new test_coverage.py locks the CoverM-parsing behaviour (which can't be
exercised without CoverM installed); test_metabat2 now asserts real variance
passes through and that absent variance falls back to zero. 85 pass.
The cross-sample containment matrix that ContainmentSelector depends on was only
produced by an out-of-tree script (data/mouse_rewilding/compute_containment.py).
This makes it a first-class package stage so containment selection is
self-sufficient.

- refrover/containment.py: compute_containment_matrix(read_sigs, assembly_sigs)
  computes containment(reads_i, assembly_j) via sourmash MinHash.contained_by,
  returning a sample x sample DataFrame (query_id index) ready for
  ContainmentSelector and load_containment_matrix. Plus sigs_by_sample() and
  write_containment_matrix() helpers.
- refrover containment CLI subcommand: takes --read-sketches / --assembly-sketches
  directories, writes containment_matrix.tsv. Idempotent (skips existing unless
  --force).

Uses the sourmash API rather than the analysis script's raw-JSON hash parsing —
slower but correct across sketch parameters and maintainable in-tree.

Tests: new test_containment.py builds real sourmash sketches in a tmp dir and
checks exact (asymmetric) containment values, labelling, TSV round-trip through
load_containment_matrix, and that the output feeds ContainmentSelector. 90 pass.
…electors CLI

Turns benchmark.py from a stub into a working alignment-free selector evaluator,
operationalising the data/mouse_rewilding selector-comparison analysis with the
real package selectors.

- unique_variance_explained(matrix, selected_ids): fraction of total cross-sample
  column variance the selected columns explain, via orthogonal (QR) projection of
  the full matrix onto the selected subspace. Correlated/redundant selections
  count once, so it rewards prototypes spanning independent axes of variation.
- rank_selectors(matrix, selectors, k_values, ...): feeds the same matrix
  (Jaccard or containment) to every selector for an apples-to-apples ranking.
  Skips unavailable selectors (feedback; archetype without its package) with a
  warning instead of aborting; seeds the random baseline for reproducibility.
- refrover rank-selectors CLI: takes --sketches or --containment-matrix, writes
  selector_ranking.tsv and prints the table.
- run_benchmark (Tier 2, CheckM2 MAG yield) documented as the gold standard the
  Tier-1 proxy is meant to predict; still NotImplementedError.

On the real 183-sample containment matrix the ranking reproduces the analysis:
maxmin and containment lead, greedy_var trails, variance rises with k.

Tests: test_benchmark.py covers the projection metric (all-columns=1.0, empty=0,
redundancy adds little, nested monotonicity) and ranking (columns, k-monotonicity
for nested maxmin, feedback skipped, reproducibility, containment on shared
matrix). 100 pass.
The three existing estimators (scree_elbow, similarity_gap, saturation_curve)
all run on a Jaccard matrix as a proxy. This adds the method validated on the
rewilded-mouse data: greedy forward selection by residual cross-sample variance
(incremental Gram-Schmidt), which gives a monotonically decreasing marginal-gain
curve, with the elbow at the first k whose gain drops below 10% of the first.

- _containment_saturation in adaptive_k.py; wired into estimate_k and the
  AdaptiveMethod literal.
- refrover select --adaptive-k-method gains the containment_saturation choice
  (use with a containment matrix for the intended behaviour).

On the real 183-sample containment matrix it reproduces the analysis exactly:
median k=4 (182 samples at 4, one at 5), matching compare_selectors.py.

Tests: containment_saturation added to the all-methods validity check, plus a
low-dimensional-data test and a dominant-axis saturation test. 102 pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant