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 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.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 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..1d96248 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,21 @@ 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 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 => 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 => "", @@ -896,6 +908,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 +930,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..d9c933c 100644 --- a/tests/test_cmdline.rs +++ b/tests/test_cmdline.rs @@ -3239,6 +3239,55 @@ 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_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