From e0dbaf94e44e0509362dfef806b7bf50d2646536 Mon Sep 17 00:00:00 2001 From: "R. Eric Collins" Date: Wed, 21 Oct 2026 23:42:14 -0500 Subject: [PATCH 1/3] Keep a given --min-read-percent-identity in metabat mode The metabat method replaced any --min-read-percent-identity with 97%, so a lower threshold was silently ignored. Use 97% only when none is given, and say so in the method's help. Fixes #308 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/bin/coverm.rs | 24 +++++++++++++++++------- src/cli.rs | 2 +- tests/test_cmdline.rs | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/bin/coverm.rs b/src/bin/coverm.rs index 91a0070..349bd52 100644 --- a/src/bin/coverm.rs +++ b/src/bin/coverm.rs @@ -1679,13 +1679,23 @@ impl FilterParameters { pub fn add_metabat_filtering_if_required(&mut self, m: &clap::ArgMatches) { if doing_metabat(m) { - info!( - "Setting single read percent identity threshold at 0.97 for \ - MetaBAT adjusted coverage, and not filtering out supplementary, \ - secondary and improper pair alignments" - ); - // we use >= where metabat uses >. Gah. - self.min_percent_identity_single = 0.97001; + // A threshold given with --min-read-percent-identity is kept; the + // 97% default only replaces an unset one. + if m.contains_id("min-read-percent-identity") { + info!( + "Using the given single read percent identity threshold for \ + MetaBAT adjusted coverage, and not filtering out supplementary, \ + secondary and improper pair alignments" + ); + } else { + info!( + "Setting single read percent identity threshold at 0.97 for \ + MetaBAT adjusted coverage, and not filtering out supplementary, \ + secondary and improper pair alignments" + ); + // we use >= where metabat uses >. Gah. + self.min_percent_identity_single = 0.97001; + } self.flag_filters.include_improper_pairs = true; self.flag_filters.include_supplementary = true; self.flag_filters.include_secondary = true; diff --git a/src/cli.rs b/src/cli.rs index 5d6571d..6996b75 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -965,7 +965,7 @@ pub fn contig_full_help() -> Manual { &[&monospace_roff("variance"), "Variance of coverage depths"], &[&monospace_roff("length"), "Length of each contig in base pairs"], &[&monospace_roff("count"), "Number of reads aligned to each contig. Note that supplementary alignments are not counted."], - &[&monospace_roff("metabat"), "(\"MetaBAT adjusted coverage\") Coverage as defined in Kang et al 2015 https://doi.org/10.7717/peerj.1165"], + &[&monospace_roff("metabat"), "(\"MetaBAT adjusted coverage\") Coverage as defined in Kang et al 2015 https://doi.org/10.7717/peerj.1165. Reads below 97% identity are excluded unless --min-read-percent-identity is given."], &[&monospace_roff("reads_per_base"), "Number of reads aligned divided by the length of the contig"], &[&monospace_roff("anir"), "Average BLAST-like identity of mapped reads"], &[&monospace_roff("rpkm"), "Reads mapped per kilobase of contig, per million mapped reads"], diff --git a/tests/test_cmdline.rs b/tests/test_cmdline.rs index 962f440..86dcd1f 100644 --- a/tests/test_cmdline.rs +++ b/tests/test_cmdline.rs @@ -1956,6 +1956,29 @@ k141_2005182 225 1.9333 1.9333 0.0631", .unwrap(); } + #[test] + fn test_metabat_given_percent_identity_is_kept() { + // Reads below 97% identity, excluded by the metabat default, pass a + // given 90% threshold. + Assert::main_binary() + .with_args(&[ + "contig", + "-m", + "metabat", + "--min-read-percent-identity", + "90", + "-b", + "tests/data/k141_2005182.head11.bam", + ]) + .succeeds() + .stdout() + .contains( + "contigName contigLen totalAvgDepth k141_2005182.head11.bam k141_2005182.head11.bam-var +k141_2005182 225 6.04 6.04 1.12", + ) + .unwrap(); + } + #[test] fn test_deletions_count_towards_perc_id() { Assert::main_binary() From 9d33089197e7af5299334a2265e0f00d2067bd35 Mon Sep 17 00:00:00 2001 From: Ben Woodcroft Date: Fri, 23 Oct 2026 08:03:05 +1000 Subject: [PATCH 2/3] contig: Better documentation for >= vs > re metabat --- src/cli.rs | 10 ++++++++-- tests/test_cmdline.rs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 6996b75..9afeb15 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -208,7 +208,9 @@ fn add_thresholding_options(manual: Manual) -> Manual { .long("--min-read-percent-identity") .help(&format!( "Exclude reads by overall percent \ - identity e.g. 95 for 95%. {}", + identity e.g. 95 for 95%. Reads at exactly the threshold \ + are kept ({}). {}", + &monospace_roff(">="), default_roff("0") )), ) @@ -965,7 +967,11 @@ pub fn contig_full_help() -> Manual { &[&monospace_roff("variance"), "Variance of coverage depths"], &[&monospace_roff("length"), "Length of each contig in base pairs"], &[&monospace_roff("count"), "Number of reads aligned to each contig. Note that supplementary alignments are not counted."], - &[&monospace_roff("metabat"), "(\"MetaBAT adjusted coverage\") Coverage as defined in Kang et al 2015 https://doi.org/10.7717/peerj.1165. Reads below 97% identity are excluded unless --min-read-percent-identity is given."], + &[&monospace_roff("metabat"), &format!("(\"MetaBAT adjusted coverage\") Coverage as defined in Kang et al 2015 https://doi.org/10.7717/peerj.1165. By default, reads with identity not strictly greater than 97% are excluded, matching MetaBAT's {}. A given {} uses CoverM's {} instead, so {} is not identical to the default.", + &monospace_roff(">"), + &monospace_roff("--min-read-percent-identity"), + &monospace_roff(">="), + &monospace_roff("97"))], &[&monospace_roff("reads_per_base"), "Number of reads aligned divided by the length of the contig"], &[&monospace_roff("anir"), "Average BLAST-like identity of mapped reads"], &[&monospace_roff("rpkm"), "Reads mapped per kilobase of contig, per million mapped reads"], diff --git a/tests/test_cmdline.rs b/tests/test_cmdline.rs index 86dcd1f..91f1d87 100644 --- a/tests/test_cmdline.rs +++ b/tests/test_cmdline.rs @@ -1979,6 +1979,29 @@ k141_2005182 225 6.04 6.04 1.12", .unwrap(); } + #[test] + fn test_metabat_given_97_uses_greater_or_equal() { + // A given 97 uses CoverM's >=, unlike the metabat default which + // approximates MetaBAT's > via 0.97001. Same BAM is 1.9333 by default. + Assert::main_binary() + .with_args(&[ + "contig", + "-m", + "metabat", + "--min-read-percent-identity", + "97", + "-b", + "tests/data/k141_2005182.head11.bam", + ]) + .succeeds() + .stdout() + .contains( + "contigName contigLen totalAvgDepth k141_2005182.head11.bam k141_2005182.head11.bam-var +k141_2005182 225 2.9333 2.9333 0.0631", + ) + .unwrap(); + } + #[test] fn test_deletions_count_towards_perc_id() { Assert::main_binary() From 603aa472c7764e7b07123d3c9240e3e5a81bfc5d Mon Sep 17 00:00:00 2001 From: Ben Woodcroft Date: Fri, 23 Oct 2026 09:17:50 +1000 Subject: [PATCH 3/3] contig: Respect --proper-pairs-only/--exclude-supplementary in metabat mode Also print the percent identity threshold in the metabat info message and document the metabat behaviour in the relevant help text. --- src/bin/coverm.rs | 24 +++++++++++++++--------- src/cli.rs | 15 +++++++++++---- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/bin/coverm.rs b/src/bin/coverm.rs index 349bd52..70769c5 100644 --- a/src/bin/coverm.rs +++ b/src/bin/coverm.rs @@ -1683,22 +1683,28 @@ impl FilterParameters { // 97% default only replaces an unset one. if m.contains_id("min-read-percent-identity") { info!( - "Using the given single read percent identity threshold for \ - MetaBAT adjusted coverage, and not filtering out supplementary, \ - secondary and improper pair alignments" + "Using the given single read percent identity threshold of {} for \ + MetaBAT adjusted coverage", + self.min_percent_identity_single ); } else { + // we use >= where metabat uses >. Gah. + self.min_percent_identity_single = 0.97001; info!( "Setting single read percent identity threshold at 0.97 for \ - MetaBAT adjusted coverage, and not filtering out supplementary, \ - secondary and improper pair alignments" + MetaBAT adjusted coverage" ); - // we use >= where metabat uses >. Gah. - self.min_percent_identity_single = 0.97001; } - self.flag_filters.include_improper_pairs = true; - self.flag_filters.include_supplementary = true; + // Improper pair and supplementary alignments are already included + // unless the user asked otherwise with --proper-pairs-only or + // --exclude-supplementary, which are respected. Secondary + // alignments are excluded by default, so include them here. self.flag_filters.include_secondary = true; + info!( + "For MetaBAT adjusted coverage, including secondary alignments; \ + supplementary alignments included: {}; improper pair alignments included: {}", + self.flag_filters.include_supplementary, self.flag_filters.include_improper_pairs + ); } } diff --git a/src/cli.rs b/src/cli.rs index 9afeb15..4b15123 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -209,9 +209,14 @@ fn add_thresholding_options(manual: Manual) -> Manual { .help(&format!( "Exclude reads by overall percent \ identity e.g. 95 for 95%. Reads at exactly the threshold \ - are kept ({}). {}", + are kept ({}). {} With {} in {} the default is {} \ + (see the {} method); a given value is used as is.", &monospace_roff(">="), - default_roff("0") + default_roff("0"), + &monospace_roff("--methods metabat"), + &monospace_roff("coverm contig"), + &monospace_roff("97"), + &monospace_roff("metabat") )), ) .option( @@ -967,11 +972,13 @@ pub fn contig_full_help() -> Manual { &[&monospace_roff("variance"), "Variance of coverage depths"], &[&monospace_roff("length"), "Length of each contig in base pairs"], &[&monospace_roff("count"), "Number of reads aligned to each contig. Note that supplementary alignments are not counted."], - &[&monospace_roff("metabat"), &format!("(\"MetaBAT adjusted coverage\") Coverage as defined in Kang et al 2015 https://doi.org/10.7717/peerj.1165. By default, reads with identity not strictly greater than 97% are excluded, matching MetaBAT's {}. A given {} uses CoverM's {} instead, so {} is not identical to the default.", + &[&monospace_roff("metabat"), &format!("(\"MetaBAT adjusted coverage\") Coverage as defined in Kang et al 2015 https://doi.org/10.7717/peerj.1165. By default, reads with identity not strictly greater than 97% are excluded, matching MetaBAT's {}. A given {} uses CoverM's {} instead, so {} is not identical to the default. This method includes secondary alignments, and by default improper pair and supplementary alignments too; use {} or {} to exclude the latter two.", &monospace_roff(">"), &monospace_roff("--min-read-percent-identity"), &monospace_roff(">="), - &monospace_roff("97"))], + &monospace_roff("97"), + &monospace_roff("--proper-pairs-only"), + &monospace_roff("--exclude-supplementary"))], &[&monospace_roff("reads_per_base"), "Number of reads aligned divided by the length of the contig"], &[&monospace_roff("anir"), "Average BLAST-like identity of mapped reads"], &[&monospace_roff("rpkm"), "Reads mapped per kilobase of contig, per million mapped reads"],