Skip to content

Commit 9d8fed4

Browse files
logannyeclaude
andcommitted
feat(cli): rosalind locate — load + exact-match query a prebuilt index
IndexReader::open -> GenomeIndexView::locate_exact -> prints contig<TAB>pos (sorted). Memory-mapped load, no rebuild, exact-match only (not the aligner — that's B4). Verified against an in-RAM GenomeIndex::locate_exact ground truth over a multi-contig + N-bearing battery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c06d993 commit 9d8fed4

2 files changed

Lines changed: 110 additions & 1 deletion

File tree

src/main.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use rosalind::core::MemoryBudget;
1010
use rosalind::genomics::{
1111
compare_callsets, create_bam_writer, estimate_build_working_set, read_vcf_variants,
1212
render_plan_line, sort_bam_deterministic, AlignedRead, BWTAligner, BedIndex, CigarOp,
13-
CigarOpKind, GenomeIndex, IndexBuildReport, IndexWriter,
13+
CigarOpKind, GenomeIndex, IndexBuildReport, IndexReader, IndexWriter,
1414
};
1515
use rosalind::io::decompress::open_input;
1616
use rosalind::io::fasta::{FastaReader, FastaRecord};
@@ -159,6 +159,18 @@ enum Commands {
159159
#[arg(long)]
160160
memory_budget_mb: Option<u64>,
161161
},
162+
/// Locate exact occurrences of a pattern in a prebuilt index (load + query).
163+
Locate {
164+
/// Index artifact built by `rosalind index`.
165+
#[arg(long)]
166+
index: PathBuf,
167+
/// Pattern to locate (ASCII A/C/G/T/N; case-insensitive).
168+
#[arg(long)]
169+
pattern: String,
170+
/// Maximum number of candidate hits to locate.
171+
#[arg(long, default_value_t = 1024)]
172+
max_hits: usize,
173+
},
162174
}
163175

164176
#[derive(Copy, Clone, Debug, ValueEnum, Eq, PartialEq)]
@@ -270,6 +282,11 @@ fn main() -> Result<()> {
270282
output,
271283
memory_budget_mb,
272284
} => run_index(reference, output, memory_budget_mb)?,
285+
Commands::Locate {
286+
index,
287+
pattern,
288+
max_hits,
289+
} => run_locate(index, pattern, max_hits)?,
273290
}
274291

275292
Ok(())
@@ -371,6 +388,34 @@ fn run_index(reference: PathBuf, output: PathBuf, memory_budget_mb: Option<u64>)
371388
Ok(())
372389
}
373390

391+
/// Load a prebuilt index and print exact-match loci for `pattern` (B3c). This is
392+
/// a memory-mapped load + exact match — it never rebuilds the index.
393+
fn run_locate(index: PathBuf, pattern: String, max_hits: usize) -> Result<()> {
394+
let loaded = IndexReader::open(&index)
395+
.with_context(|| format!("failed to open index {}", index.display()))?;
396+
let view = loaded
397+
.genome_view()
398+
.with_context(|| format!("failed to view index {}", index.display()))?;
399+
400+
let loci = view.locate_exact(pattern.as_bytes(), max_hits);
401+
if loci.is_empty() {
402+
eprintln!("no hits");
403+
return Ok(());
404+
}
405+
406+
let mut stdout = io::BufWriter::new(io::stdout().lock());
407+
for locus in loci {
408+
let name = view
409+
.contigs()
410+
.by_id(locus.contig)
411+
.map(|c| c.name.to_string())
412+
.unwrap_or_else(|| locus.contig.to_string());
413+
writeln!(stdout, "{name}\t{}", locus.pos.0)?;
414+
}
415+
stdout.flush()?;
416+
Ok(())
417+
}
418+
374419
fn run_somatic(
375420
reference_path: PathBuf,
376421
tumor_fastq: Option<PathBuf>,

tests/index_cli.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,67 @@ fn index_memory_budget_prints_plan_line_and_never_refuses() {
106106

107107
std::fs::remove_dir_all(&dir).ok();
108108
}
109+
110+
#[test]
111+
fn locate_matches_in_ram_ground_truth() {
112+
use rosalind::genomics::GenomeIndex;
113+
114+
let dir = tmpdir();
115+
let fa = write_fasta(&dir);
116+
let idx = dir.join("ref.idx");
117+
let build = Command::new(bin())
118+
.args([
119+
"index",
120+
"--reference",
121+
fa.to_str().unwrap(),
122+
"--output",
123+
idx.to_str().unwrap(),
124+
])
125+
.output()
126+
.expect("index");
127+
assert!(build.status.success());
128+
129+
// In-RAM ground truth over the same sequences.
130+
let gi = GenomeIndex::from_named_sequences(&[
131+
(
132+
"chr1".to_string(),
133+
b"ACGTACGTNNACGTACGTACGTAAGGCCTT".to_vec(),
134+
),
135+
(
136+
"chr2".to_string(),
137+
b"TTTTGGGGCCCCAAAANNNNACGTACGTAC".to_vec(),
138+
),
139+
(
140+
"chr3".to_string(),
141+
b"GATTACATTTTGATTACAGGGGGCCCCAAA".to_vec(),
142+
),
143+
])
144+
.unwrap();
145+
146+
for pat in ["GATTACA", "ACGT", "GGGGG", "NNNN", "TTTTGGGG", "ZZZZ"] {
147+
let out = Command::new(bin())
148+
.args(["locate", "--index", idx.to_str().unwrap(), "--pattern", pat])
149+
.output()
150+
.expect("locate");
151+
assert!(
152+
out.status.success(),
153+
"locate {pat} failed: {}",
154+
String::from_utf8_lossy(&out.stderr)
155+
);
156+
let stdout = String::from_utf8_lossy(&out.stdout);
157+
158+
let mut expected: Vec<String> = gi
159+
.locate_exact(pat.as_bytes(), 1024)
160+
.into_iter()
161+
.map(|l| {
162+
let name = gi.contigs().by_id(l.contig).unwrap().name.to_string();
163+
format!("{name}\t{}", l.pos.0)
164+
})
165+
.collect();
166+
expected.sort();
167+
let mut got: Vec<String> = stdout.lines().map(|s| s.to_string()).collect();
168+
got.sort();
169+
assert_eq!(got, expected, "locate mismatch for {pat}");
170+
}
171+
std::fs::remove_dir_all(&dir).ok();
172+
}

0 commit comments

Comments
 (0)