Skip to content

Commit 5164724

Browse files
authored
Merge pull request #21 from logannye/rosalind/phase-c-contract
Phase C — memory as a verifiable contract + the front door (Move #4)
2 parents aa35243 + fe594b9 commit 5164724

23 files changed

Lines changed: 5014 additions & 146 deletions

CONTRACT.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# The memory contract
2+
3+
**Memory is a contract, not a hope.**
4+
5+
Every other variant caller treats RAM as an emergent property you guess at (`-Xmx…`,
6+
`--target-mem` "heuristics may not work well") and then crash on. Rosalind treats it as a contract:
7+
8+
> **Rosalind never silently OOM-kills you — it fits, or it tells you up front, and it proves the
9+
> realized peak with a receipt.**
10+
11+
(It does *not* claim to "never refuse": when a budget is genuinely too small the run declines cleanly
12+
rather than crashing. Graceful degrade-don't-die — sliding down a space/time curve to finish anyway — is
13+
the Phase-D research direction, not a present claim.)
14+
15+
## The four verbs
16+
17+
The contract applies to the bounded whole-genome germline path, `rosalind variants --index`.
18+
19+
### 1. Declare
20+
21+
State the RAM you have. `--memory-budget-mb N` on `variants`, `--budget-mb N` on `plan`/`verify`.
22+
23+
### 2. Predict — `rosalind plan`
24+
25+
Ask *before committing a byte* whether the job fits. `plan` reads only the index header (plus your
26+
declared depth/read-length assumptions) — it never opens the BAM:
27+
28+
```bash
29+
rosalind plan --index genome.idx --max-depth 1000 --max-read-len 250 --budget-mb 2048
30+
```
31+
32+
It prints a breakdown — reference decode + active set @ max-depth + engine overhead, atop a measured
33+
process baseline — and a verdict: `[FITS]` or `[REFUSE]`.
34+
35+
### 3. Honor — `rosalind variants … --enforce`
36+
37+
```bash
38+
rosalind variants --index genome.idx --alignments sample.sorted.bam \
39+
--memory-budget-mb 2048 --enforce -o sample.vcf
40+
```
41+
42+
With `--enforce`:
43+
44+
- **predicted peak > budget → refuse up front** (exit **3**), before doing any work, with an actionable
45+
message (raise the budget, lower `--max-depth`, or drop `--enforce`);
46+
- **realized peak > budget → fail loud** (exit **4**) *after* writing the VCF + receipt (you keep the data
47+
and the proof it overran) — never a silent overrun;
48+
- otherwise the run completes within budget.
49+
50+
Without `--enforce`, the budget is **record-only**: the run always completes and the verdict is recorded in
51+
the receipt. The active read set is capped at `--max-depth` (default 1000; `0` = uncapped) — deterministic
52+
downsampling that bounds the working set; output changes only at sites deeper than the cap.
53+
54+
### 4. Verify — `rosalind verify`
55+
56+
Re-check a receipt *without re-running*:
57+
58+
```bash
59+
rosalind verify --manifest sample.vcf.manifest.json
60+
```
61+
62+
It re-hashes the recorded inputs and outputs (BLAKE3) and re-checks the recorded peak against the budget
63+
(supplied via `--budget-mb`, or read from the manifest). Exit **0** if everything matches and fits;
64+
non-zero (exit **5**) with a per-check report on any drift, missing file, or over-budget peak. This is the
65+
auditability story containers can't give you for a non-deterministic caller.
66+
67+
## What's bounded (honest scope)
68+
69+
- **Germline `variants --index`** is the bounded path: peak ≈ the largest contig's reference + the
70+
depth-capped active set, **independent of BAM size**. Reads stream one record at a time.
71+
- **Somatic** (`somatic`) is **region-bounded**, not whole-genome-bounded (it collects both pileup streams
72+
for the region).
73+
- **Index *build*** (`rosalind index`) is **O(reference)** in RAM today; `plan --reference` reports an
74+
advisory estimate. Sublinear-space construction is the Phase-D research direction (see
75+
`docs/OPEN_PROBLEMS.md`).
76+
- The engine is **single-threaded** — outputs are deterministic, but there is no thread-invariance claim
77+
yet.
78+
79+
## Extend — build on the bounded substrate
80+
81+
Rosalind's kernel is a bounded, deterministic **`PileupColumn` stream**. Compute your own per-locus
82+
analytics over it (coverage, QC, methylation, ML features) and inherit bounded memory + determinism for
83+
free — no variant calling required:
84+
85+
```rust
86+
use rosalind::{PileupEngine, PileupParams};
87+
// PileupEngine<S: ReadSource> is an Iterator<Item = Result<PileupColumn, _>>.
88+
// Each PileupColumn carries the locus, ref_base, depth(), allele_counts(), strand_counts().
89+
for column in PileupEngine::new(source, reference, contig, region, PileupParams::default()) {
90+
let col = column?;
91+
// your bounded per-locus metric here
92+
}
93+
```
94+
95+
A complete, runnable example: [`examples/custom_pileup_analytics.rs`](examples/custom_pileup_analytics.rs)
96+
(`cargo run --example custom_pileup_analytics`).
97+
98+
> **Legacy / non-bounded.** The `GenomicPlugin` trait (`src/plugin/`), the `framework/` evaluator, and the
99+
> Python `run_rna_seq_plugin` demo still work but do **not** inherit the memory contract. Prefer the
100+
> `PileupColumn` substrate above for bounded work.
101+
102+
## Reproducibility
103+
104+
Every receipt is canonical JSON (sorted keys, no timestamps) with BLAKE3 content hashes of the index, the
105+
alignments, and the output VCF, plus the realized `peak_rss_bytes` / `max_working_set_bytes` and the
106+
contract params (`memory_budget_mb`, `contract_verdict`, `enforced`, `max_depth`, `max_read_len`). Identical
107+
inputs produce a byte-identical VCF and a byte-identical manifest — and `rosalind verify` proves it.

README.md

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,29 @@ rosalind index --reference genome.fa --output genome.idx
1919
# 2. (Align reads with your favorite aligner and coordinate-sort the BAM.)
2020
# `rosalind sort` will do the sort deterministically within a memory budget.
2121

22-
# 3. Call germline variants across the WHOLE genome, streaming, bounded.
22+
# 3. Will it fit in 4 GB? Ask before committing a byte.
23+
rosalind plan --index genome.idx --max-depth 1000 --budget-mb 4096
24+
25+
# 4. Call germline variants across the WHOLE genome, streaming, honoring the budget.
2326
rosalind variants \
2427
--index genome.idx \
2528
--alignments sample.sorted.bam \
26-
--memory-budget-mb 4096 \
29+
--memory-budget-mb 4096 --enforce \
2730
-o sample.vcf
2831
# ...writes a multi-contig VCF, plus to stderr:
2932
# memory: peak RSS 412 MiB; max pileup working set 18 KiB
33+
# contract: OK — realized peak 412 MiB within declared 4096 MiB
3034
# wrote reproducibility receipt: sample.vcf.manifest.json
35+
36+
# 5. Re-check the receipt later — no re-run — to prove it fit and is reproducible.
37+
rosalind verify --manifest sample.vcf.manifest.json
3138
```
3239

3340
What makes this different:
3441

3542
- **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.
3643
- **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)*.
44+
- **A contract, honored.** `rosalind plan` predicts the peak *before you commit a byte*; `--enforce` honors the budget — refusing up front (exit 3) or failing loud (exit 4) rather than silently OOM-killing you; `rosalind verify` re-checks the receipt without re-running. Without `--enforce`, the budget is record-only. The full story: [the memory contract](CONTRACT.md).
3845
- **Reproducible + auditable.** Identical inputs produce a byte-identical VCF; a BLAKE3 manifest records the index, the BAM, the output, and the memory used.
3946

4047
---
@@ -48,7 +55,7 @@ What makes this different:
4855
- **Deterministic coordinate sort**`rosalind sort`: an external merge sort (spills to disk) that orders a BAM by position within a configurable memory budget.
4956
- **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.
5057
- **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.
58+
- **Extensibility**Build custom bounded per-locus analytics over the `PileupColumn` iterator substrate (see [`examples/custom_pileup_analytics.rs`](examples/custom_pileup_analytics.rs)), inheriting bounded memory + determinism for free. *(The legacy `GenomicPlugin` trait + PyO3 RNA-seq demo still work but are **not** memory-bounded — see [CONTRACT.md](CONTRACT.md).)*
5259
- **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).
5360

5461
## Why it matters
@@ -59,7 +66,7 @@ Three properties, treated as first-class guarantees rather than nice-to-haves:
5966
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.
6067
3. **Honest uncertainty.** Calibrated, abstention-aware calling refuses to emit a call where the evidence is insufficient, instead of papering over it.
6168

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.
69+
The contract is real today: `rosalind plan` predicts before you commit, `--enforce` honors the budget, and `rosalind verify` re-checks the receipt (see [CONTRACT.md](CONTRACT.md)). 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, aimed at **sublinear-space index *construction*** — the future Phase-D direction that would extend the contract to the index build step. That layer is not yet load-bearing; the bounded streaming engine you use today is the practical foundation it builds on.
6370

6471
## Who it's for
6572

@@ -73,16 +80,17 @@ Under the hood, Rosalind is also a research vehicle for **space-bounded genomics
7380
- **Whole-genome:** germline variant calling via `rosalind variants --index` (all contigs, streaming, bounded) and exact-match lookup via `rosalind index` / `rosalind locate`.
7481
- **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.)
7582
- 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).
83+
- The engine runs **single-threaded** today. `--memory-budget-mb` is record-only by default; add `--enforce` to honor it (refuse up front / fail loud — see [CONTRACT.md](CONTRACT.md)).
7784

7885
## Roadmap
7986

8087
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."
8188

8289
- **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.
8390
- **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.
91+
- **Phase C (done — in review):** memory as a *verifiable contract*`rosalind plan` (a checkable envelope before you commit), `--enforce` (honor-or-refuse: refuse up front / fail loud, never a silent OOM-kill), and `rosalind verify`. See [CONTRACT.md](CONTRACT.md).
92+
- **Phase D (research):** sublinear-space index construction — the `~√t` space/time knob across the full curve — extending the contract to the index *build* step (today's build is O(reference)). The headline space-complexity bet; see [`docs/OPEN_PROBLEMS.md`](docs/OPEN_PROBLEMS.md).
93+
- **Later:** the aligner over the persisted multi-contig index (`align --index`, whole-genome alignment); germline indels and richer read QC; deterministic multithreading; a Python/tensor binding over the pileup stream.
8694

8795
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).
8896

@@ -125,16 +133,32 @@ python scripts/generate_toy_data.py examples/data/illumina_toy
125133
# Build the index once.
126134
rosalind index --reference genome.fa --output genome.idx
127135

128-
# Call across all contigs from a coordinate-sorted BAM, in bounded memory.
136+
# Predict the peak before committing; then call all contigs, honoring the budget.
137+
rosalind plan --index genome.idx --max-depth 1000 --budget-mb 4096
129138
rosalind variants \
130139
--index genome.idx \
131140
--alignments sample.sorted.bam \
132141
--mapq-threshold 20 \
133-
--memory-budget-mb 4096 \
142+
--memory-budget-mb 4096 --enforce \
134143
-o sample.vcf
144+
rosalind verify --manifest sample.vcf.manifest.json
135145
```
136146

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.
147+
`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. With `--enforce` the declared budget is honored (refuse up front / fail loud); without it, it is record-only. The full contract: [CONTRACT.md](CONTRACT.md).
148+
149+
### Try the contract end-to-end (bundled data, in-house tools only)
150+
151+
```bash
152+
D=examples/data/illumina_toy
153+
rosalind index --reference $D/reference.fa --output /tmp/toy.idx
154+
rosalind sort --input $D/alignments.bam --output /tmp/toy.sorted.bam
155+
rosalind plan --index /tmp/toy.idx --budget-mb 512
156+
rosalind variants --index /tmp/toy.idx --alignments /tmp/toy.sorted.bam \
157+
--memory-budget-mb 512 --enforce -o /tmp/toy.vcf
158+
rosalind verify --manifest /tmp/toy.vcf.manifest.json
159+
```
160+
161+
This bundled demo is **single-contig** because Rosalind's own aligner is single-contig. For **whole-genome** calling, align with bwa-mem2/minimap2, coordinate-sort, and bring the BAM to `variants --index` — which calls *every* contig in bounded memory.
138162

139163
### Single-contig alignment + calling
140164

@@ -154,6 +178,8 @@ Inputs may be plain or gzip/bgzf-compressed (auto-detected); pass `-` to read FA
154178

155179
Run `rosalind <subcommand> --help` for exact flags.
156180

181+
- `rosalind plan` — predict a job's peak memory vs a declared budget *before* committing (`--index` for the variants peak, `--reference` for the index build).
182+
- `rosalind verify` — re-check a reproducibility receipt without re-running: re-hash its inputs/outputs and confirm the realized peak landed within budget.
157183
- `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.
158184
- `rosalind sort` — deterministic coordinate sort of a BAM within a memory budget.
159185
- `rosalind somatic` — tumor/normal somatic SNV + simple-indel calling from a paired BAM set over a region.
@@ -239,9 +265,12 @@ depth = engine.run_rna_seq_plugin(
239265

240266
## Extend
241267

242-
- **Rust plugins** — implement `GenomicPlugin` (see `src/plugin/examples.rs`) to run custom per-block analyses (coverage, QC counts, domain-specific summaries) on the same bounded-memory evaluator.
243-
- **CLI subcommands** — add workflows in `src/main.rs`.
244-
- **Python** — drive the engine from `rosalind_py.PyGenomicEngine` alongside pandas / NumPy / scikit-learn.
268+
Rosalind's kernel is a **bounded, deterministic `PileupColumn` stream** — build your own per-locus analytics (coverage, QC, methylation, ML features) over it and inherit bounded memory + determinism for free:
269+
270+
- **Rust (recommended)** — consume the `PileupEngine` iterator over any `ReadSource`. See [`examples/custom_pileup_analytics.rs`](examples/custom_pileup_analytics.rs) (`cargo run --example custom_pileup_analytics`) for a non-caller consumer computing per-locus coverage. The contract verbs and the substrate are re-exported at the crate root (`use rosalind::{PileupEngine, PileupColumn, ReadSource, …}`).
271+
- **CLI subcommands** — add workflows in `src/main.rs`; compose subcommands over pipes.
272+
273+
> **Legacy / non-bounded.** The `GenomicPlugin` trait (`src/plugin/`), the `framework/` evaluator, and the Python `rosalind_py.PyGenomicEngine` RNA-seq demo still work but do **not** inherit the memory contract. Prefer the `PileupColumn` substrate above for bounded work. See [CONTRACT.md](CONTRACT.md).
245274
246275
---
247276

0 commit comments

Comments
 (0)