From b6170576b1a1829d17d11a9e6fa9cb62a5c40d68 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 09:52:53 +0000 Subject: [PATCH 1/4] Add rammap as a mapping option rammap (https://github.com/jwanglab/rammap) is a pure-Rust, minimap2-compatible read mapper available on Bioconda. Wire it in as a new `--mapper rammap` choice alongside the existing mappers. - Add MappingProgram::RAMMAP and handle it in every mapping-program match (command construction, index maintenance, parameter selection). - Map short reads with `rammap -x sr -a`, matching CoverM's default short-read use case, and expose `--rammap-params` for extra options. - Add check_for_rammap dependency/version check. - Register rammap in the CLI mapper list, help table, README dependency list, and pixi test environment; add a `make` smoke test. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D4S2TfmfT9BsYgJF5VDz81 --- README.md | 1 + pixi.toml | 1 + src/bam_generator.rs | 18 +++++++++++++---- src/bin/coverm.rs | 11 +++++++++++ src/cli.rs | 33 ++++++++++++++++++++++++++++++++ src/external_command_checker.rs | 7 +++++++ src/mapping_index_maintenance.rs | 10 ++++++++-- src/mapping_parameters.rs | 1 + tests/test_cmdline.rs | 24 +++++++++++++++++++++++ 9 files changed, 100 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 68d2618..1347d30 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ and one of these for mapping: * [strobealign](https://github.com/ksahlin/StrobeAlign) v0.14.0 * [minimap2](https://github.com/lh3/minimap2) v2.21 * [bwa-mem2](https://github.com/bwa-mem2/bwa-mem2) v2.0 +* [rammap](https://github.com/jwanglab/rammap) v1.1.1 and one of these for genome dereplication: * [skani](https://github.com/bluenote-1577/skani) v0.1.1 diff --git a/pixi.toml b/pixi.toml index 92089de..b54f738 100644 --- a/pixi.toml +++ b/pixi.toml @@ -28,6 +28,7 @@ extern = "*" bwa-mem2 = "*" dashing = ">=0.4.0,<1.0" # dashing 1.0 build gives illegal instruction errors strobealign = ">=0.14.0" # tests generate the strobealign index at runtime +rammap = ">=1.1.1" [package] name = "coverm" diff --git a/src/bam_generator.rs b/src/bam_generator.rs index 5c1b953..36129e1 100644 --- a/src/bam_generator.rs +++ b/src/bam_generator.rs @@ -54,6 +54,7 @@ pub enum MappingProgram { MINIMAP2_LR_HQ, MINIMAP2_NO_PRESET, STROBEALIGN, + RAMMAP, } pub struct BamFileNamedReader { @@ -416,7 +417,10 @@ pub fn generate_named_bam_readers_from_reads( // Required because of https://github.com/wwood/CoverM/issues/58 let minimap2_log_file_index = match mapping_program { - MappingProgram::BWA_MEM | MappingProgram::BWA_MEM2 | MappingProgram::STROBEALIGN => None, + MappingProgram::BWA_MEM + | MappingProgram::BWA_MEM2 + | MappingProgram::STROBEALIGN + | MappingProgram::RAMMAP => None, // Required because of https://github.com/lh3/minimap2/issues/527 MappingProgram::MINIMAP2_SR | MappingProgram::MINIMAP2_ONT @@ -867,13 +871,15 @@ pub fn build_mapping_command( mapping_options: Option<&str>, ) -> String { let read_params1 = match mapping_program { - // minimap2 auto-detects interleaved based on read names + // minimap2 (and the minimap2-compatible rammap) auto-detect + // interleaved input based on read names MappingProgram::MINIMAP2_SR | MappingProgram::MINIMAP2_ONT | MappingProgram::MINIMAP2_HIFI | MappingProgram::MINIMAP2_PB | MappingProgram::MINIMAP2_LR_HQ - | MappingProgram::MINIMAP2_NO_PRESET => "", + | MappingProgram::MINIMAP2_NO_PRESET + | MappingProgram::RAMMAP => "", MappingProgram::BWA_MEM | MappingProgram::BWA_MEM2 => match read_format { ReadFormat::Interleaved => "-p", ReadFormat::Coupled | ReadFormat::Single => "", @@ -896,6 +902,9 @@ pub fn build_mapping_command( MappingProgram::BWA_MEM => "bwa mem".to_string(), MappingProgram::BWA_MEM2 => "bwa-mem2 mem".to_string(), MappingProgram::STROBEALIGN => "strobealign".to_string(), + // rammap is a minimap2-compatible aligner; map short reads with + // the 'sr' preset and emit SAM with -a. + MappingProgram::RAMMAP => "rammap -x sr -a".to_string(), _ => { let split_prefix = tempfile::Builder::new() .prefix("coverm-minimap2-split-index") @@ -915,7 +924,8 @@ pub fn build_mapping_command( match mapping_program { MappingProgram::BWA_MEM | MappingProgram::BWA_MEM2 - | MappingProgram::STROBEALIGN => unreachable!(), + | MappingProgram::STROBEALIGN + | MappingProgram::RAMMAP => unreachable!(), MappingProgram::MINIMAP2_SR => "-x sr", MappingProgram::MINIMAP2_ONT => "-x map-ont", MappingProgram::MINIMAP2_HIFI => "-x map-hifi", diff --git a/src/bin/coverm.rs b/src/bin/coverm.rs index 1fa08e8..b6e7211 100644 --- a/src/bin/coverm.rs +++ b/src/bin/coverm.rs @@ -800,6 +800,13 @@ fn setup_mapping_index( ) } } + MappingProgram::RAMMAP => { + // Pre-generating an index for a batch of readsets is not supported for rammap + info!("Not pre-generating rammap index"); + Box::new(coverm::mapping_index_maintenance::VanillaIndexStruct::new( + reference_wise_params.reference, + )) + } MappingProgram::STROBEALIGN => { // Indexing once for a batch of readsets is not yet supported for strobealign info!("Not pre-generating strobealign index"); @@ -881,6 +888,7 @@ fn parse_mapping_program(m: &clap::ArgMatches) -> MappingProgram { Some("minimap2-lr-hq") => MappingProgram::MINIMAP2_LR_HQ, Some("minimap2-no-preset") => MappingProgram::MINIMAP2_NO_PRESET, Some("strobealign") => MappingProgram::STROBEALIGN, + Some("rammap") => MappingProgram::RAMMAP, None => DEFAULT_MAPPING_SOFTWARE_ENUM, _ => panic!( "Unexpected definition for --mapper: {:?}", @@ -905,6 +913,9 @@ fn parse_mapping_program(m: &clap::ArgMatches) -> MappingProgram { MappingProgram::STROBEALIGN => { external_command_checker::check_for_strobealign(); } + MappingProgram::RAMMAP => { + external_command_checker::check_for_rammap(); + } } mapping_program } diff --git a/src/cli.rs b/src/cli.rs index 1793e36..62588d4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,6 +21,7 @@ const MAPPING_SOFTWARE_LIST: &[&str] = &[ "minimap2-lr-hq", "minimap2-no-preset", "strobealign", + "rammap", ]; const DEFAULT_MAPPING_SOFTWARE: &str = "strobealign"; @@ -94,6 +95,10 @@ fn add_mapping_options(manual: Manual) -> Manual { &monospace_roff("minimap2-no-preset"), &format!("minimap2 with no '{}' option", &monospace_roff("-x")) ], + &[ + &monospace_roff("rammap"), + &format!("rammap (a minimap2-compatible aligner) with '{}' option", &monospace_roff("-x sr")) + ], ]) ))) .option(Opt::new("PARAMS").long("--minimap2-params").help(&format!( @@ -120,6 +125,13 @@ fn add_mapping_options(manual: Manual) -> Manual { implications if untrusted input is specified. \ [default: none]", )) + .option(Opt::new("PARAMS").long("--rammap-params").help(&format!( + "Extra parameters to provide to rammap. Note \ + that usage of this parameter has security \ + implications if untrusted input is specified. '{}' \ + is always specified to rammap. [default: none]", + &monospace_roff("-x sr -a") + ))) .flag(Flag::new().long("--strobealign-use-index").help( "Use a pregenerated index (one that has been created with 'strobealign --create-index'). The --reference option should be specified as the original FASTA file i.e. 'ref.fna' not 'ref.fna.r100.sti' [default: not set]", )), @@ -1244,6 +1256,13 @@ Ben J. Woodcroft .requires("reference") .action(clap::ArgAction::SetTrue), ) + .arg( + Arg::new("rammap-params") + .long("rammap-params") + .long("rammap-parameters") + .allow_hyphen_values(true) + .requires("reference"), + ) // TODO: Relax this for autoconcatenation .arg( Arg::new("discard-unmapped") @@ -1779,6 +1798,13 @@ Ben J. Woodcroft .requires("reference") .action(clap::ArgAction::SetTrue), ) + .arg( + Arg::new("rammap-params") + .long("rammap-params") + .long("rammap-parameters") + .allow_hyphen_values(true) + .requires("reference"), + ) .arg( Arg::new("discard-unmapped") .long("discard-unmapped") @@ -2156,6 +2182,13 @@ Ben J. Woodcroft .long("strobealign-use-index") .requires("reference") .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("rammap-params") + .long("rammap-params") + .long("rammap-parameters") + .allow_hyphen_values(true) + .requires("reference"), ), ) .subcommand( diff --git a/src/external_command_checker.rs b/src/external_command_checker.rs index 54976ce..558d361 100644 --- a/src/external_command_checker.rs +++ b/src/external_command_checker.rs @@ -30,3 +30,10 @@ pub fn check_for_strobealign() { default_version_check("strobealign", "0.11.0", false, None) .expect("Failed to find sufficient version of strobealign"); } + +pub fn check_for_rammap() { + check_for_external_command_presence_with_which("rammap") + .expect("Failed to find installed rammap"); + default_version_check("rammap", "1.1.1", false, None) + .expect("Failed to find sufficient version of rammap"); +} diff --git a/src/mapping_index_maintenance.rs b/src/mapping_index_maintenance.rs index 64f6c3f..c9f06c3 100644 --- a/src/mapping_index_maintenance.rs +++ b/src/mapping_index_maintenance.rs @@ -68,6 +68,7 @@ impl TemporaryIndexStruct { | MappingProgram::MINIMAP2_LR_HQ | MappingProgram::MINIMAP2_NO_PRESET => std::process::Command::new("minimap2"), MappingProgram::STROBEALIGN => std::process::Command::new("strobealign"), + MappingProgram::RAMMAP => std::process::Command::new("rammap"), }; match &mapping_program { MappingProgram::BWA_MEM | MappingProgram::BWA_MEM2 => { @@ -101,7 +102,8 @@ impl TemporaryIndexStruct { MappingProgram::MINIMAP2_NO_PRESET | MappingProgram::BWA_MEM | MappingProgram::BWA_MEM2 - | MappingProgram::STROBEALIGN => {} + | MappingProgram::STROBEALIGN + | MappingProgram::RAMMAP => {} }; if let Some(t) = num_threads { cmd.arg("-t").arg(format!("{t}")); @@ -111,6 +113,9 @@ impl TemporaryIndexStruct { MappingProgram::STROBEALIGN => { warn!("STROBEALIGN pre-indexing is not supported currently, so skipping index generation."); } + MappingProgram::RAMMAP => { + warn!("RAMMAP pre-indexing is not supported currently, so skipping index generation."); + } }; if let Some(params) = index_creation_options { for s in params.split_whitespace() { @@ -207,7 +212,8 @@ pub fn check_reference_existence(reference_path: &str, mapping_program: &Mapping | MappingProgram::MINIMAP2_PB | MappingProgram::MINIMAP2_LR_HQ | MappingProgram::MINIMAP2_NO_PRESET - | MappingProgram::STROBEALIGN => {} + | MappingProgram::STROBEALIGN + | MappingProgram::RAMMAP => {} }; if !ref_path.exists() { diff --git a/src/mapping_parameters.rs b/src/mapping_parameters.rs index c762152..30ca938 100644 --- a/src/mapping_parameters.rs +++ b/src/mapping_parameters.rs @@ -125,6 +125,7 @@ impl<'a> MappingParameters<'a> { | MappingProgram::MINIMAP2_LR_HQ | MappingProgram::MINIMAP2_NO_PRESET => "minimap2-params", MappingProgram::STROBEALIGN => "strobealign-params", + MappingProgram::RAMMAP => "rammap-params", }; let mapping_options = match m.contains_id(mapping_parameters_arg) { true => { diff --git a/tests/test_cmdline.rs b/tests/test_cmdline.rs index dc4cc6b..d9b5caf 100644 --- a/tests/test_cmdline.rs +++ b/tests/test_cmdline.rs @@ -3239,6 +3239,30 @@ genome6~random_sequence_length_11003 0 0 0 .is_file()); } + #[test] + fn test_make_rammap() { + let td = tempfile::TempDir::new().unwrap(); + Assert::main_binary() + .with_args(&[ + "make", + "--coupled", + "tests/data/reads_for_seq1_and_seq2.1.fq.gz", + "tests/data/reads_for_seq1_and_seq2.2.fq.gz", + "--reference", + "tests/data/7seqs.fna", + "--mapper", + "rammap", + "--output-directory", + td.path().to_str().unwrap(), + ]) + .succeeds() + .unwrap(); + assert!(td + .path() + .join("7seqs.fna.reads_for_seq1_and_seq2.1.fq.gz.bam") + .is_file()); + } + #[test] fn test_strobealign_pregenerated_index() { // Generate the index at runtime so the format always matches the installed strobealign version From af164a3770e566af84bc2be2a0fd6b133d93bd83 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 09:56:33 +0000 Subject: [PATCH 2/4] Skip rammap tests on macOS CI rammap, like bwa-mem2 and strobealign, is only installed on linux-64 (it is not available for osx via bioconda). The macOS CI jobs already skip the bwa_mem2 and strobealign tests for this reason; add rammap to that skip list so test_make_rammap does not fail for a missing executable. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D4S2TfmfT9BsYgJF5VDz81 --- .github/workflows/test-coverm.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-coverm.yml b/.github/workflows/test-coverm.yml index 7956e11..35d542c 100644 --- a/.github/workflows/test-coverm.yml +++ b/.github/workflows/test-coverm.yml @@ -84,7 +84,7 @@ jobs: - name: Run tests with Pixi run: | - pixi run --frozen cargo test -- --skip bwa_mem2 --skip strobealign + pixi run --frozen cargo test -- --skip bwa_mem2 --skip strobealign --skip rammap # Run after removing lock file so dependences are unlocked pixi_test_dependencies_optimistic_osx: @@ -117,4 +117,4 @@ jobs: - name: Run tests with Pixi run: | - pixi run cargo test -- --skip bwa_mem2 --skip strobealign + pixi run cargo test -- --skip bwa_mem2 --skip strobealign --skip rammap From 1cec469030f6957bef2a747cab7c6f6d7e4c43f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 10:05:02 +0000 Subject: [PATCH 3/4] Map single-end reads correctly with rammap rammap's `sr` preset enables fragment mode, which pulls read pairs from a single input file (this is how rammap maps interleaved reads). For genuinely single-end input (`--single`) that would incorrectly consume the file as interleaved pairs and drop an odd trailing read. Pass `--frag no` for ReadFormat::Single so single-end reads are mapped individually, while coupled and interleaved input keep the default fragment-mode behaviour. Add a single-end rammap smoke test covering this path. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D4S2TfmfT9BsYgJF5VDz81 --- src/bam_generator.rs | 14 ++++++++++---- tests/test_cmdline.rs | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/bam_generator.rs b/src/bam_generator.rs index 36129e1..1d96248 100644 --- a/src/bam_generator.rs +++ b/src/bam_generator.rs @@ -871,15 +871,21 @@ pub fn build_mapping_command( mapping_options: Option<&str>, ) -> String { let read_params1 = match mapping_program { - // minimap2 (and the minimap2-compatible rammap) auto-detect - // interleaved input based on read names + // minimap2 auto-detects interleaved input based on read names MappingProgram::MINIMAP2_SR | MappingProgram::MINIMAP2_ONT | MappingProgram::MINIMAP2_HIFI | MappingProgram::MINIMAP2_PB | MappingProgram::MINIMAP2_LR_HQ - | MappingProgram::MINIMAP2_NO_PRESET - | MappingProgram::RAMMAP => "", + | MappingProgram::MINIMAP2_NO_PRESET => "", + MappingProgram::RAMMAP => match read_format { + // rammap's `sr` preset turns on fragment mode, which pulls read + // pairs from a single input file (this is how it maps interleaved + // reads). For genuinely single-end reads that would incorrectly + // consume the file as interleaved pairs, so disable fragment mode. + ReadFormat::Single => "--frag no", + ReadFormat::Coupled | ReadFormat::Interleaved => "", + }, MappingProgram::BWA_MEM | MappingProgram::BWA_MEM2 => match read_format { ReadFormat::Interleaved => "-p", ReadFormat::Coupled | ReadFormat::Single => "", diff --git a/tests/test_cmdline.rs b/tests/test_cmdline.rs index d9b5caf..d9c933c 100644 --- a/tests/test_cmdline.rs +++ b/tests/test_cmdline.rs @@ -3263,6 +3263,31 @@ genome6~random_sequence_length_11003 0 0 0 .is_file()); } + #[test] + fn test_make_rammap_single() { + // Single-end reads must not be consumed as interleaved pairs by + // rammap's fragment mode; exercise the single-end path end to end. + let td = tempfile::TempDir::new().unwrap(); + Assert::main_binary() + .with_args(&[ + "make", + "--single", + "tests/data/reads_for_seq1_and_seq2.1.fq.gz", + "--reference", + "tests/data/7seqs.fna", + "--mapper", + "rammap", + "--output-directory", + td.path().to_str().unwrap(), + ]) + .succeeds() + .unwrap(); + assert!(td + .path() + .join("7seqs.fna.reads_for_seq1_and_seq2.1.fq.gz.bam") + .is_file()); + } + #[test] fn test_strobealign_pregenerated_index() { // Generate the index at runtime so the format always matches the installed strobealign version From a3c791b06d7cb76279de38db1277691c25b7f1e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 18:49:12 +0000 Subject: [PATCH 4/4] Add rammap to pixi.lock Commit the rammap linux-64 conda package (1.1.1, build hfa8f182_0) into the frozen lockfile so `pixi run --frozen` installs it in the locked CI job, matching how strobealign and bwa-mem2 are pinned. Package hash and metadata taken from the bioconda repodata and verified against the downloaded artifact (sha256 4e03e5..., 885279 bytes). Its only non-system dependency, libgcc >=14, is already satisfied by libgcc 15.1.0 in the lock. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D4S2TfmfT9BsYgJF5VDz81 --- pixi.lock | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pixi.lock b/pixi.lock index c2df8fa..87f1a74 100644 --- a/pixi.lock +++ b/pixi.lock @@ -56,6 +56,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/bioconda/linux-64/rammap-1.1.1-hfa8f182_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/bioconda/linux-64/samtools-1.21-h50ea8bc_0.tar.bz2 - conda: https://conda.anaconda.org/bioconda/linux-64/skani-0.2.2-ha6fb395_2.tar.bz2 @@ -1445,6 +1446,18 @@ packages: license_family: BSD size: 6960 timestamp: 1752805923703 +- conda: https://conda.anaconda.org/bioconda/linux-64/rammap-1.1.1-hfa8f182_0.conda + sha256: 4e03e5be5ae8044d73f2e2b4b6f3e2fc6aa64da9252ef8446cb96bd796913840 + md5: a80c1a98ed68ab781a95ab5b9202896e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 885279 + timestamp: 1780841211866 - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c md5: 283b96675859b20a825f8fa30f311446