Skip to content

Refresh the custom/orfcollapse fixture for the positional orf_class vocabulary - #2193

Merged
FelixKrueger merged 3 commits into
nf-core:modulesfrom
FelixKrueger:fix/orfcollapse-fixture-positional-orf-class
Aug 3, 2026
Merged

Refresh the custom/orfcollapse fixture for the positional orf_class vocabulary#2193
FelixKrueger merged 3 commits into
nf-core:modulesfrom
FelixKrueger:fix/orfcollapse-fixture-positional-orf-class

Conversation

@FelixKrueger

Copy link
Copy Markdown

The custom/orfcollapse fixture still carries orf_class = smORF, which nf-core/riboseq's ORF catalogue no longer produces — a short ORF now keeps its positional class (uORF, dORF, canonical_cds, …) and a separate is_smorf column carries the length flag. The catalogue also gained an orf_type_native column holding each caller's own ORF-type label. This updates cohort.catalogue.tsv to that schema, so the three small ORFs become uORF/uORF/dORF and both new columns are present. The BED12, ORF-to-gene map, peptide FASTA and cluster TSV are unchanged, and the module test still collapses the same two identical-peptide rows onto the same survivor.

AI-assisted detail — keep or bin as you see fit

Needed because custom/orfcollapse now aborts on an orf_class value it does not recognise, rather than silently omitting those rows from its per-class MultiQC table. With smORF gone from the module's vocabulary, the current fixture fails the guard.

Row-level changes to cohort.catalogue.tsv:

orf_id was now is_smorf orf_type_native
orf_00000001 smORF uORF 1 5'UTR
orf_00000002 smORF uORF 1 uORF
orf_00000003 canonical_cds canonical_cds 0 Annotated
orf_00000004 smORF dORF 1 3'UTR

orf_00000001 and orf_00000002 keep the same class on purpose. They are the identical-peptide pair the fixture exists to exercise, and orfcollapse prefers the more specific class when a peptide cluster spans several — so giving them different classes would change which row survives the fold and break the test's orf_00000001 survivor assertions.

cohort.catalogue.mqc.tsv is regenerated over the module's current class order. It is not consumed by any test, but leaving smORF in it would misinform the next reader.

Verified by running the module template against the updated fixture: all thirteen assertions in custom/orfcollapse's main.nf.test hold — 4 rows collapse to 3, orf_00000002 folds into orf_00000001, the survivor unions both callers and both samples, the ORF-to-gene map remaps g5/t5 onto the survivor, and the peptide FASTA keeps only the survivor. The survivor's orf_type_native becomes 5'UTR,uORF, the union of the folded members' labels.

The corresponding pipeline change is not yet open as a PR; this fixture update has to land first, since the module test cannot go green without it.

…class vocabulary

nf-core/riboseq's custom/orfmerge no longer emits the length-derived `smORF`
orf_class: a short ORF keeps its positional class and carries a separate
`is_smorf` length flag. The catalogue also gained an `orf_type_native` column
holding each caller's own ORF-type label.

This fixture still carried `orf_class = smORF` and lacked both new columns, so
custom/orfcollapse aborted on it -- the module now rejects an orf_class value it
does not know rather than silently dropping those rows from its MultiQC table.

The two identical-peptide rows keep the same class so the same row still survives
the fold; the BED12, ORF-to-gene map, peptide FASTA and cluster TSV are unchanged.
…class vocabulary

nf-core/riboseq's custom/orfmerge no longer emits the length-derived `smORF`
orf_class: a short ORF keeps its positional class and carries a separate
`is_smorf` length flag. The catalogue also gained an `orf_type_native` column
holding each caller's own ORF-type label.

This fixture still carried `orf_class = smORF` and lacked both new columns, so
custom/orfcollapse aborted on it -- the module now rejects an orf_class value it
does not know rather than silently dropping those rows from its MultiQC table.

The two identical-peptide rows keep the same class so the same row still survives
the fold; the BED12, ORF-to-gene map, peptide FASTA and cluster TSV are unchanged.
…class vocabulary

nf-core/riboseq's custom/orfmerge no longer emits the length-derived `smORF`
orf_class: a short ORF keeps its positional class and carries a separate
`is_smorf` length flag. The catalogue also gained an `orf_type_native` column
holding each caller's own ORF-type label.

This fixture still carried `orf_class = smORF` and lacked both new columns, so
custom/orfcollapse aborted on it -- the module now rejects an orf_class value it
does not know rather than silently dropping those rows from its MultiQC table.

The two identical-peptide rows keep the same class so the same row still survives
the fold; the BED12, ORF-to-gene map, peptide FASTA and cluster TSV are unchanged.

@erikrikarddaniel erikrikarddaniel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

@FelixKrueger
FelixKrueger merged commit c2daf9e into nf-core:modules Aug 3, 2026
2 checks passed
@FelixKrueger
FelixKrueger deleted the fix/orfcollapse-fixture-positional-orf-class branch August 3, 2026 09:31
FelixKrueger added a commit to FelixKrueger/nf-core-modules that referenced this pull request Aug 3, 2026
custom/orfnormalise overwrote orf_class with "smORF" for every ORF at or under
100 aa, so a short uORF, a short downstream ORF, a short truncated CDS variant
and a short novel intergenic ORF were indistinguishable in exactly the size range
microprotein work cares about. Because custom/orfmerge keys its clustering
strategy on orf_class and custom/orfcollapse scoped its peptide dedup to that
value, the length override also decided how an ORF merged across callers.

orf_class is now purely positional, and length moves to a separate is_smorf flag
driven by a new --smorf-max-aa (default 100, reproducing the previous membership
exactly). The vocabulary gains uoORF, doORF and intORF, which the callers were
already reporting: RiboCode and ribotricer emit Overlap_uORF/Overlap_dORF,
RiboCode and Ribo-TISH emit internal, PRICE emits uoORF and iORF. ORF-type tokens
are now matched exactly rather than by substring, which is what previously folded
overlap_uORF into uORF.

orfmerge partitions by clustering strategy with orf_class absent from every
grouping key, because callers disagree on class for the same ORF -- Ribo-TISH
reports 5'UTR for both uORFs and CDS-overlapping uORFs -- and keying on class
emits one row per disagreeing caller. Reciprocal overlap is measured on summed
exon-block intersection rather than the outer span, which for a spliced ORF is
mostly intron, and linkage is complete so a chain of partial overlaps cannot fold
distinct ORFs together. Unknown classes and unassigned rows now abort.

orfcollapse derives its eligible set from aa_length plus --smorf-max-aa rather
than a propagated flag, and prefers the more specific class when a peptide
cluster spans several, so an annotated CDS is not deleted by a longer novel ORF
sharing its peptide.

Adds orf_type_native, carrying each caller's own label through to the catalogue
so every harmonisation decision stays auditable.

Requires the refreshed orfcollapse fixture from nf-core/test-datasets#2193.
LouisLeNezet pushed a commit to LouisLeNezet/modules that referenced this pull request Aug 6, 2026
…-core#12498)

* Decouple orf_class from ORF length across the custom/orf* modules

custom/orfnormalise overwrote orf_class with "smORF" for every ORF at or under
100 aa, so a short uORF, a short downstream ORF, a short truncated CDS variant
and a short novel intergenic ORF were indistinguishable in exactly the size range
microprotein work cares about. Because custom/orfmerge keys its clustering
strategy on orf_class and custom/orfcollapse scoped its peptide dedup to that
value, the length override also decided how an ORF merged across callers.

orf_class is now purely positional, and length moves to a separate is_smorf flag
driven by a new --smorf-max-aa (default 100, reproducing the previous membership
exactly). The vocabulary gains uoORF, doORF and intORF, which the callers were
already reporting: RiboCode and ribotricer emit Overlap_uORF/Overlap_dORF,
RiboCode and Ribo-TISH emit internal, PRICE emits uoORF and iORF. ORF-type tokens
are now matched exactly rather than by substring, which is what previously folded
overlap_uORF into uORF.

orfmerge partitions by clustering strategy with orf_class absent from every
grouping key, because callers disagree on class for the same ORF -- Ribo-TISH
reports 5'UTR for both uORFs and CDS-overlapping uORFs -- and keying on class
emits one row per disagreeing caller. Reciprocal overlap is measured on summed
exon-block intersection rather than the outer span, which for a spliced ORF is
mostly intron, and linkage is complete so a chain of partial overlaps cannot fold
distinct ORFs together. Unknown classes and unassigned rows now abort.

orfcollapse derives its eligible set from aa_length plus --smorf-max-aa rather
than a propagated flag, and prefers the more specific class when a peptide
cluster spans several, so an annotated CDS is not deleted by a longer novel ORF
sharing its peptide.

Adds orf_type_native, carrying each caller's own label through to the catalogue
so every harmonisation decision stays auditable.

Requires the refreshed orfcollapse fixture from nf-core/test-datasets#2193.

* Address the nf-core#12498 review: cross-strategy merging and flag-driven collapse

Class selects the clustering strategy in orfmerge, so two callers agreeing on
an ORF's exact structure but disagreeing on its class were partitioned into
different strategies and could never merge -- the de-merge this PR set out to
remove, surviving one level down. Rows sharing an identical exon structure are
now grouped first and clustered as a single proxy chosen by class specificity,
with the members restored afterwards. Output is byte-identical on every
existing fixture, so a synthetic test pins the new behaviour.

orfcollapse takes eligibility from the catalogue's is_smorf rather than
re-deriving it, and requires the column to be present and 0/1. --smorf-max-aa
is kept solely to re-derive the flag and abort when the two disagree, so a
threshold passed to one process and not the other fails loudly instead of
silently narrowing the collapse scope. This keeps the flag and the length on a
row in agreement, which deriving is_smorf from the row's own aa_length needs.

orfnormalise aborts on an unmapped ORF-type token instead of counting it: the
token falls through to `other` and selects the wrong merge strategy. Explicit
`other` mappings are unaffected, since they report as matched.

Also corrects two shipped docs against the code: orfcollapse's representative
is chosen by class specificity before length, and orfmerge's reciprocal overlap
is measured on summed exon-block intersection, not the outer genomic span. The
two one-off orfnormalise test configs are replaced by a shared
tests/nextflow.config exposing params.module_args.

* Validate ORF classes on the input rows, not the merge proxies

The exact-structure pre-grouping moved the unknown-class guard onto proxies.
representative() ranks an unknown class last, so at a shared structure a known
class always won the proxy and the unknown one escaped the guard entirely, then
write_catalogue re-emitted the row under the known class -- the silent swallow
the guard exists to prevent. Validate the input rows before grouping.

Also states orfnormalise's fail-fast behaviour in meta.yml: an unmatched token
aborts before outputs are written, so a non-zero unmapped_orf_type can never
appear in the provenance line.
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.

2 participants