Skip to content

Commit aa35243

Browse files
authored
Merge pull request #20 from logannye/rosalind/docs-readme-refresh
docs: refresh README for bounded whole-genome `variants --index`
2 parents b39dcbe + ab4b270 commit aa35243

1 file changed

Lines changed: 99 additions & 69 deletions

File tree

README.md

Lines changed: 99 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,106 @@
11
# Rosalind
22

3-
A deterministic, low-memory genomics engine in Rust for read alignment and variant calling on commodity hardware.
3+
**A deterministic, low-memory genomics engine in Rust — call variants across a whole genome on a laptop, with memory you can predict and verify, and results that are byte-for-byte reproducible.**
44

5-
Rosalind streams variant calling over coordinate-sorted alignments with a working set bounded by local read coverage rather than by file size, and emits BAM/VCF that are designed to be bit-for-bit reproducible across runs. It is built to be embedded and extended: a Rust library and CLI you can call directly, add plugins to, or drive from Python.
5+
Most variant callers use memory that grows with your data, so "will this finish on my machine?" is something you find out the hard way. Rosalind is built around a different promise: **memory as a contract you can see before you commit and verify after the run.** It streams a coordinate-sorted BAM one read at a time, reads the reference from a compact portable index (no second copy of the genome in RAM), keeps its working set proportional to *local read depth* rather than file size, and prints a receipt showing the memory it actually used. Run it twice on the same inputs and you get identical output, bit for bit. And where the evidence is too thin to be sure, it **abstains** instead of guessing.
6+
7+
It's a Rust **library and CLI** you can call directly, extend with plugins, or drive from Python — not a black-box pipeline.
8+
9+
---
10+
11+
## The headline: bounded whole-genome calling from a portable index
12+
13+
You already have an aligner you trust (bwa-mem2, minimap2, or Rosalind's own). Bring Rosalind a **coordinate-sorted BAM** and a **prebuilt index**, and it calls germline variants across **every contig** in bounded, predictable memory:
14+
15+
```bash
16+
# 1. Build a portable, memory-mappable index of your reference — once.
17+
rosalind index --reference genome.fa --output genome.idx
18+
19+
# 2. (Align reads with your favorite aligner and coordinate-sort the BAM.)
20+
# `rosalind sort` will do the sort deterministically within a memory budget.
21+
22+
# 3. Call germline variants across the WHOLE genome, streaming, bounded.
23+
rosalind variants \
24+
--index genome.idx \
25+
--alignments sample.sorted.bam \
26+
--memory-budget-mb 4096 \
27+
-o sample.vcf
28+
# ...writes a multi-contig VCF, plus to stderr:
29+
# memory: peak RSS 412 MiB; max pileup working set 18 KiB
30+
# wrote reproducibility receipt: sample.vcf.manifest.json
31+
```
32+
33+
What makes this different:
34+
35+
- **Bounded memory, independent of BAM size.** Reads stream one record at a time; peak memory is roughly *the largest contig's reference + the local pileup working set* — not the size of your alignments. A human genome calls comfortably on a laptop.
36+
- **Self-contained.** The reference comes from the `.idx`; you don't need the original FASTA at call time.
37+
- **A memory receipt.** Every run reports its realized peak RSS and max pileup working set — to stderr and into a reproducibility manifest. `--memory-budget-mb` flags a run that exceeds your declared budget *(it records the verdict; it does not yet abort — enforcement is on the roadmap)*.
38+
- **Reproducible + auditable.** Identical inputs produce a byte-identical VCF; a BLAKE3 manifest records the index, the BAM, the output, and the memory used.
639

740
---
841

942
## What it does today
1043

11-
- **Alignment** — Builds a Burrows–Wheeler / FM-index (SA-IS suffix array with blocked rank/select) over a reference contig and aligns reads via exact-match seeding, deterministic diagonal chaining, and banded affine-gap refinement. Emits SAM or BGZF-compressed BAM.
12-
- **Streaming I/O** — Reads plain or gzip/bgzf-compressed FASTA/FASTQ, auto-detected from the magic bytes; FASTQ can stream from stdin (`-`). Parsing lives in the `io::` layer (`io::fasta`, `io::fastq`).
13-
- **Coordinate sort** — A deterministic external merge sort (spills to disk) that orders a BAM by position within a configurable memory budget.
14-
- **Germline variant calling** — Streams a pileup over a coordinate-sorted BAM and calls SNVs to VCF, keeping the in-memory working set proportional to coverage, not to the size of the input.
15-
- **Somatic (tumor/normal) calling** — Calls somatic SNVs and simple indels from a paired tumor/normal BAM set using a deterministic binomial log-likelihood-ratio model with explicit depth and allele-fraction filters.
16-
- **Truth-set evaluation** — Compares a call set against a truth VCF over confident regions (BED), with variant normalization (left-align + trim) and precision / recall / F1.
17-
- **Extensibility** — Implement the `GenomicPlugin` trait to run custom per-block analyses on the same bounded-memory evaluator (an example RNA-seq coverage plugin is included), or call the PyO3 bindings from Python.
18-
- **Determinism** — Primary artifacts are emitted in a canonical, stable order and are designed to be byte-for-byte identical across repeated runs given identical inputs and configuration. See [`docs/determinism.md`](docs/determinism.md).
44+
- **Bounded whole-genome germline calling**`rosalind variants --index` streams a coordinate-sorted BAM over all contigs of a persisted index, calling SNVs to a multi-contig VCF with a working set bounded by coverage. Calls are calibrated and **abstention-aware** (no confident call → no row, rather than a guess).
45+
- **Build-once, query-many index**`rosalind index` builds a portable, memory-mapped FM-index over a (multi-contig) reference; `rosalind locate` answers exact-match queries against it in milliseconds. The index is **never rebuilt on load** and is **byte-identical across builds** of the same reference.
46+
- **Alignment**`rosalind align` builds a Burrows–Wheeler / FM-index over a reference contig and aligns reads via exact-match seeding, deterministic diagonal chaining, and banded affine-gap refinement. Emits SAM or BGZF-compressed BAM.
47+
- **Streaming I/O** — Reads plain or gzip/bgzf-compressed FASTA/FASTQ, auto-detected from the magic bytes; FASTQ can stream from stdin (`-`).
48+
- **Deterministic coordinate sort**`rosalind sort`: an external merge sort (spills to disk) that orders a BAM by position within a configurable memory budget.
49+
- **Somatic (tumor/normal) calling**`rosalind somatic` calls somatic SNVs and simple indels from a paired tumor/normal BAM set using a deterministic binomial log-likelihood-ratio model with explicit depth and allele-fraction filters.
50+
- **Truth-set evaluation**`rosalind eval-somatic` compares a call set against a truth VCF over confident regions (BED), with variant normalization (left-align + trim) and precision / recall / F1.
51+
- **Extensibility** — Implement the `GenomicPlugin` trait to run custom per-block analyses on the same bounded-memory evaluator, or call the PyO3 bindings from Python.
52+
- **Determinism by design** — Primary artifacts are emitted in a canonical, stable order, byte-for-byte identical across repeated runs given identical inputs. See [`docs/determinism.md`](docs/determinism.md).
1953

20-
## Current scope
54+
## Why it matters
2155

22-
At the **command line**, Rosalind currently operates on a **single reference contig per run**, reads plain or **gzip/bgzf-compressed** FASTQ/FASTA (auto-detected, including from stdin), and runs **single-threaded**. Variant calling is **single-sample** (germline) or a **tumor/normal pair** (somatic); calling is SNV-focused, with simple indels in the somatic path. Alignment uses exact-match seeding. The FM-index is built in memory at the start of each run (memory proportional to the reference); the bounded-memory property applies to the streaming pileup and variant-calling stages. These boundaries define what the engine targets well today — small-to-moderate references, targeted regions, and per-sample streaming workloads.
56+
Three properties, treated as first-class guarantees rather than nice-to-haves:
2357

24-
The library also provides a multi-contig FM-index over the concatenated genome (`genomics::GenomeIndex`) that resolves matches to `(contig, position)`; it is exposed via `rosalind index` / `rosalind locate` (wiring multi-contig through `align`/`variants` is a later phase). See the roadmap below.
58+
1. **Predictable memory.** The bet is that for a growing set of users — sequencing in the field, in the clinic, on a laptop, or at genome scale on modest hardware — *"it fits, and I knew it would"* matters more than raw throughput. Rosalind makes the streaming working set bounded by local coverage and surfaces the realized peak so the bound is **verifiable, not just claimed**.
59+
2. **Reproducibility.** Byte-identical outputs and a per-run BLAKE3 manifest make results auditable — a hard requirement for clinical and regulated pipelines, and a sanity-saver for everyone else.
60+
3. **Honest uncertainty.** Calibrated, abstention-aware calling refuses to emit a call where the evidence is insufficient, instead of papering over it.
61+
62+
Under the hood, Rosalind is also a research vehicle for **space-bounded genomics**: a `~√t` (square-root-space) evaluation framework as a continuous space/time knob — trade time for memory along a curve a declared budget selects. That direction (sublinear-space index *construction*, budget *enforcement*, `rosalind plan`/`verify`) is on the roadmap below; the bounded streaming engine you can use today is the practical foundation it builds on.
2563

2664
## Who it's for
2765

28-
- **Edge, field, and low-resource settings** — sequencing on a laptop or portable device where large servers aren't available and predictable memory matters more than peak throughput.
66+
- **Edge, field, and low-resource settings** — sequencing on a laptop or portable device where predictable memory matters more than peak throughput.
2967
- **Reproducibility-sensitive work** — pipelines where byte-identical, auditable outputs are a first-class requirement.
68+
- **Builders** — anyone who wants a hackable Rust genomics engine to embed, extend with plugins, or drive from Python.
3069
- **Teaching and learning** — a readable, end-to-end Rust implementation of FM-index alignment, streaming pileup, and variant calling to study, modify, and extend.
31-
- **Builders** — anyone who wants a hackable Rust genomics engine to embed, extend with plugins, or drive from Python, rather than a black-box pipeline.
32-
- **Somatic SNV / simple-indel exploration** on small references and targeted regions.
70+
71+
## Current scope (what's single-contig vs. whole-genome today)
72+
73+
- **Whole-genome:** germline variant calling via `rosalind variants --index` (all contigs, streaming, bounded) and exact-match lookup via `rosalind index` / `rosalind locate`.
74+
- **Single-contig:** Rosalind's own **aligner** (`rosalind align`) and the FASTA-based `variants --reference` path operate on one reference contig per run. For whole-genome calling, align with any standard aligner and bring the coordinate-sorted BAM to `variants --index`. (Wiring the *aligner* onto the persisted multi-contig index is a later phase — see the roadmap.)
75+
- Variant calling is **single-sample** (germline) or a **tumor/normal pair** (somatic); calling is SNV-focused, with simple indels in the somatic path.
76+
- The engine runs **single-threaded** today. `--memory-budget-mb` is **record-only** (it reports a verdict but does not yet enforce).
3377

3478
## Roadmap
3579

36-
The core primitive is a streaming, CIGAR-aware pileup column stream; variant calling and custom plugins consume it.
80+
The core primitive is a streaming, CIGAR-aware pileup column stream; variant calling and custom plugins consume it. Performance work deliberately *follows* the unique capability — the target user needs "it fits and is predictable" before "it's fastest."
3781

38-
- **Phase A (done):** the streaming pileup engine; calibrated, abstention-aware germline SNV calling; tumor/normal somatic SNV calling; spec-valid VCF output; a BLAKE3 reproducibility receipt per run.
39-
- **Phase B (in progress):** streaming gzip/bgzf input, a multi-contig FM-index over the concatenated genome (`genomics::GenomeIndex`, with `(contig, position)` resolution and boundary-aware exact-match lookup), and a build-once, memory-mapped index (`rosalind index` to build, `rosalind locate` to query — never rebuilds, byte-identically reproducible) have landed. Next: wiring multi-contig through the `align`/`variants` CLI (whole-genome alignment and calling), and pipe-native composition across subcommands.
40-
- **Later:** germline indel calling and richer read QC; deterministic multithreading with an enforced memory budget; a Python binding exposing the pileup stream.
82+
- **Phase A (done):** the streaming pileup engine; calibrated, abstention-aware germline SNV calling; tumor/normal somatic calling; spec-valid VCF; a BLAKE3 reproducibility receipt per run.
83+
- **Phase B (done):** streaming gzip/bgzf input; a multi-contig FM-index over the concatenated genome with `(contig, position)` resolution; a build-once, memory-mapped, byte-reproducible persisted index (`rosalind index`/`locate`); zero-copy reference access from the index; and **bounded whole-genome germline calling over a sorted BAM** (`rosalind variants --index`) with a realized-memory receipt.
84+
- **Phase C (next):** memory as an *enforceable* contract — `rosalind plan` (a checkable memory envelope before you commit), budget **enforcement** with graceful degradation (never OOM on a real device), and `rosalind verify`.
85+
- **Later:** sublinear-space index construction (the `~√t` space/time knob across the full curve); the aligner over the persisted multi-contig index (`align --index`, whole-genome alignment); germline indels and richer read QC; deterministic multithreading; a Python binding over the pileup stream.
4186

42-
Target architecture and per-phase plans: [`docs/superpowers/specs/`](docs/superpowers/specs/), [`docs/superpowers/plans/`](docs/superpowers/plans/).
87+
Target architecture and per-phase specs/plans live in [`docs/superpowers/specs/`](docs/superpowers/specs/) and [`docs/superpowers/plans/`](docs/superpowers/plans/); the guiding thesis is in [`docs/OPEN_PROBLEMS.md`](docs/OPEN_PROBLEMS.md).
4388

4489
---
4590

4691
## Install & build
4792

4893
### Prerequisites
4994
- Rust 1.72+ (`rustup` recommended)
50-
- Native compression headers for BAM output: `libbz2-dev` & `liblzma-dev` on Debian/Ubuntu, `brew install bzip2 xz` on macOS
95+
- Native compression headers for BAM I/O: `libbz2-dev` & `liblzma-dev` on Debian/Ubuntu, `brew install bzip2 xz` on macOS
5196
- Python 3.9+ (only for the PyO3 bindings; set `PYO3_PYTHON=/path/to/python` if the default interpreter is unsuitable)
5297

5398
### Build
5499
```bash
55100
git clone https://github.com/logannye/rosalind.git
56101
cd rosalind
57102
cargo build --release
58-
cargo test # run the full suite
103+
cargo test # run the full suite
59104
cargo run --release -- --help
60105
```
61106

@@ -74,63 +119,45 @@ python scripts/generate_toy_data.py examples/data/illumina_toy
74119

75120
## Use it
76121

77-
### Command line
122+
### Whole-genome germline calling (the flagship path)
78123

79124
```bash
80-
# 1. Align FASTQ reads to a reference contig → SAM (stdout) or BAM (to disk)
81-
cargo run --release -- align \
82-
--reference examples/data/ref.fa \
83-
--reads examples/data/reads.fastq \
84-
--format sam \
85-
--max-mismatches 2 > examples/data/alignments.sam
86-
87-
cargo run --release -- align \
88-
--reference examples/data/ref.fa \
89-
--reads examples/data/reads.fastq \
90-
--format bam \
91-
--output examples/data/alignments.bam
92-
93-
# 2. Call germline SNVs from the alignments → VCF (stdout, or --output FILE)
94-
cargo run --release -- variants \
95-
--reference examples/data/ref.fa \
96-
--alignments examples/data/alignments.sam \
97-
--mapq-threshold 10
98-
```
99-
100-
Other subcommands (run `rosalind <subcommand> --help` for exact flags):
101-
- `rosalind sort` — deterministic coordinate sort of a BAM within a memory budget.
102-
- `rosalind somatic` — tumor/normal somatic SNV + simple-indel calling from a paired BAM set over a region.
103-
- `rosalind eval-somatic` — compare a call set to a truth VCF over confident regions.
125+
# Build the index once.
126+
rosalind index --reference genome.fa --output genome.idx
104127

105-
`align` indexes the first FASTA record; additional records are ignored with a warning (single-contig scope). `variants` reads coordinate-sorted SAM/BAM alignments. Inputs may be plain or gzip/bgzf-compressed (auto-detected); pass `-` to read FASTQ from stdin, e.g. `gzip -dc reads.fastq.gz | rosalind align --reads - --reference ref.fa --format sam`.
128+
# Call across all contigs from a coordinate-sorted BAM, in bounded memory.
129+
rosalind variants \
130+
--index genome.idx \
131+
--alignments sample.sorted.bam \
132+
--mapq-threshold 20 \
133+
--memory-budget-mb 4096 \
134+
-o sample.vcf
135+
```
106136

107-
## Build once, query many: the persisted index
137+
`variants --index` requires a **coordinate-sorted BAM** (use `rosalind sort` or `samtools sort`). It reads the reference from the index — no `--reference` FASTA needed — and writes a multi-contig VCF plus a memory + reproducibility receipt. `--memory-budget-mb` records (does not yet enforce) a verdict against the realized peak.
108138

109-
Build a portable, memory-mappable index from a (multi-contig) reference once:
139+
### Single-contig alignment + calling
110140

111141
```bash
112-
rosalind index --reference genome.fa --output genome.idx
113-
# index: genome.idx
114-
# contigs: 3 (90 bp total)
115-
# chr1 30
116-
# ...
117-
# reference_blake3: <hex>
118-
# index_bytes: <n>
142+
# Align FASTQ reads to a single reference contig → SAM (stdout) or BAM (to disk).
143+
rosalind align --reference examples/data/ref.fa --reads examples/data/reads.fastq \
144+
--format bam --output examples/data/alignments.bam
145+
146+
# Call germline SNVs from a single-contig reference + sorted alignments → VCF.
147+
rosalind variants --reference examples/data/ref.fa \
148+
--alignments examples/data/alignments.sam --mapq-threshold 10
119149
```
120150

121-
Then query it in milliseconds — it is memory-mapped, never rebuilt:
151+
Inputs may be plain or gzip/bgzf-compressed (auto-detected); pass `-` to read FASTQ from stdin, e.g. `gzip -dc reads.fastq.gz | rosalind align --reads - --reference ref.fa --format sam`. `align` indexes the first FASTA record (single-contig scope).
122152

123-
```bash
124-
rosalind locate --index genome.idx --pattern GATTACA
125-
# chr3 0
126-
# chr3 11
127-
```
153+
### Other subcommands
128154

129-
`rosalind index` is deterministic (the `.idx` is byte-identical across builds of
130-
the same reference). `--memory-budget-mb M` prints a record-only build plan line
131-
(`[OK]`/`[OVER]`) — it does not yet enforce the budget (that is a later phase).
132-
`locate` is exact-match only; seed/chain/extend alignment against the persisted
133-
index lands in a later phase.
155+
Run `rosalind <subcommand> --help` for exact flags.
156+
157+
- `rosalind locate --index genome.idx --pattern GATTACA` — exact-match positions in a prebuilt index (memory-mapped, never rebuilt). Exact-match only; seed/chain/extend alignment against the persisted index is a later phase.
158+
- `rosalind sort` — deterministic coordinate sort of a BAM within a memory budget.
159+
- `rosalind somatic` — tumor/normal somatic SNV + simple-indel calling from a paired BAM set over a region.
160+
- `rosalind eval-somatic` — compare a call set to a truth VCF over confident regions.
134161

135162
### Rust API
136163

@@ -185,6 +212,8 @@ fn locate(query: &[u8]) -> Result<Vec<Locus>, GenomeIndexError> {
185212
}
186213
```
187214

215+
The bounded whole-genome drive (`rosalind::call::call_germline_whole_genome`) wraps the per-contig caller above: it streams a sorted read source over a persisted index's `ReferenceView`, calling every contig in a single pass and returning the calls plus the max working set observed.
216+
188217
### Python
189218

190219
```bash
@@ -220,6 +249,7 @@ depth = engine.run_rna_seq_plugin(
220249

221250
```bash
222251
cargo test # full unit + integration suite
252+
cargo test --test variants_index # bounded whole-genome `variants --index` gates
223253
cargo test --test determinism # byte-identical outputs across repeated runs
224254
cargo test --test space_bounds # working-set scaling checks for the streaming evaluator
225255
cargo test --test fm_index_props # property tests: FM-index rank/total invariants vs. naive counts

0 commit comments

Comments
 (0)