Skip to content

Add --gff flag for per-gene coverage in contig and genome modes#298

Open
wwood wants to merge 2 commits into
mainfrom
gff
Open

Add --gff flag for per-gene coverage in contig and genome modes#298
wwood wants to merge 2 commits into
mainfrom
gff

Conversation

@wwood

@wwood wwood commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a --gff option to coverm contig and coverm genome. When one or more GFF3 files are supplied, CoverM reports coverage statistics per feature (gene) rather than per-contig (contig mode) or per-genome (genome mode).

coverm contig -b mapping.bam --gff annotation.gff > per_gene_coverage.tsv
coverm genome -b mapping.bam -s '~' --gff annotation.gff -m mean

How it works

  • New src/gene.rs contains a small GFF3 parser (read_gff_files) and gene_coverage(), which mirrors contig_coverage but computes coverage over each gene's region on its parent contig.
  • Each gene's per-base coverage is reconstructed from the contig's coverage array and fed through the existing CoverageEstimators, so all coverage methods work (mean, trimmed_mean, covered_fraction, covered_bases, variance, length, count, reads_per_base, rpkm, tpm, anir, ...).
  • Reads are attributed to genes by reference overlap (a position-sorted sweep), yielding per-gene read counts, edit distances and identities.
  • Feature names come from the GFF ID attribute (falling back to Name, then seqid_start_end). The output uses a Gene header with one row per feature.
  • The GFF seqid (column 1) must match a BAM reference sequence name. The trailing ##FASTA section of a GFF3 file is ignored.

Tests

  • Unit tests in src/gene.rs for the GFF parser and gene coverage (a gene spanning a whole contig reproduces the whole-contig coverage; per-gene read counts).
  • assert_cli integration tests in tests/test_cmdline.rs for both contig --gff and genome --gff (mean, length and count methods).
  • Test GFF files added under tests/data/genes/.

All new tests pass; cargo clippy --fix and cargo fmt applied.

🤖 Generated with Claude Code

Add a --gff option to `coverm contig` and `coverm genome` that accepts one
or more GFF3 files. When provided, CoverM reports coverage statistics for
each feature (gene) defined in the GFF files rather than per-contig or
per-genome.

Coverage for each gene is computed over its region on the parent contig
(GFF seqid), reusing the existing coverage estimators, so all coverage
methods (mean, trimmed_mean, covered_fraction, variance, length, count,
reads_per_base, rpkm, tpm, anir, ...) are supported. Reads are attributed
to genes by reference overlap, giving per-gene read counts and identities.

- New src/gene.rs: GFF3 parser and gene_coverage() function.
- Wire --gff into the contig and genome subcommands and full help.
- Output uses a "Gene" header and one row per feature.
- Add test GFF files (tests/data/genes/) plus unit and assert_cli tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013JBSD2WGRUBCErFRkjjeCB

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f99a1f578

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gene.rs
for estimator in coverage_estimators.iter_mut() {
estimator.setup();
estimator.add_contig(
&gene_ups_and_downs,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid applying contig-end exclusion to gene slices

When --gff is used with the default --contig-end-exclusion 75, each gene is converted into its own gene_ups_and_downs slice and passed to CoverageEstimator::add_contig, whose mean/trimmed/variance estimators drop the first/last contig_end_exclusion bases of the slice. That makes any covered gene ≤150 bp report 0 coverage and trims 150 bp from every longer gene in the README's default --gff example; the exclusion should stay tied to contig ends or be forced to 0 in gene mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e65462b. Contig-end exclusion is now disabled for the coverage estimators when --gff is used (new CoverageEstimator::disable_contig_end_exclusion()), since gene boundaries are not contig ends. Added a unit test asserting a whole-contig gene reports the same value with --contig-end-exclusion 75 as with 0.

Comment thread src/gene.rs
if is_primary {
gene.num_mapped_reads += 1;
if aligned_len > 0 {
gene.sum_identity += identity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit ANIr to the gene-overlap bases

For reads that span multiple genes, the code computes identity/NM over the entire alignment and then adds that same value to every overlapped gene. With --gff -m anir, a mismatch or indel in one gene will lower the ANIr reported for an adjacent gene that the read also overlaps, even when the bases inside that gene match perfectly; compute identity/edit distance over the read's overlap with each gene instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered, but keeping whole-read attribution deliberately. The per-read edit distance comes from the NM tag, which is a whole-read statistic and cannot be split positionally between genes without parsing MD tags / the reference. Prorating NM by overlap length reduces mathematically back to the whole-read identity, so it isn't genuinely more accurate. The alternative (only counting reads fully contained in a gene) avoids cross-gene contamination but silently drops boundary-spanning reads from ANIr. Since genes are typically much longer than reads, we've opted to keep the simple whole-read attribution and documented the limitation in a code comment.

Addresses PR review: in gene mode each gene is a sub-region of a contig, so
the default --contig-end-exclusion (75) was trimming the ends of every gene
and zeroing out any covered gene shorter than 150 bp. Contig-end exclusion is
now disabled for the coverage estimators when --gff is used, since gene
boundaries are not contig ends.

Add CoverageEstimator::disable_contig_end_exclusion() and a unit test that a
whole-contig gene reports the same coverage with contig-end-exclusion 75 as
with 0.

Per-gene edit distance / ANIr continue to use whole-read NM statistics (NM
cannot be split positionally between genes), documented in a comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013JBSD2WGRUBCErFRkjjeCB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant