Skip to content

Store translation exceptions and transl_table per transcript - #132

Draft
Hoeze wants to merge 1 commit into
SACGF:mainfrom
Hoeze:feat/translation-exceptions
Draft

Hoeze wants to merge 1 commit into
SACGF:mainfrom
Hoeze:feat/translation-exceptions

Conversation

@Hoeze

@Hoeze Hoeze commented Sep 25, 2026

Copy link
Copy Markdown

🤖 cdot JSON gets two optional transcript keys: transl_except (e.g. {"Sec": [48]}) and transl_table (e.g. 2). With them, a client can read UGA as selenocysteine only at the annotated codons, can see which stop codons the poly(A) tail completes, and can pick the genetic code per transcript. Older clients ignore both keys. The data schema goes to 0.2.35, a patch bump.

Why

  • Selenoproteins. For RefSeq, cdot JSON has only the note "UGA stop codon recoded as selenocysteine", and for Ensembl only the seleno tag. A client can then only switch the whole transcript to a selenocysteine table. Every UGA then reads as selenocysteine, including a UGA that a variant creates. A TGG>TGA variant then gives p.Trp5Sec instead of p.Trp5Ter.
  • Stop codons completed by the poly(A) tail. RefSeq writes transl_except=(pos:...,aa:TERM) on these CDS rows. cdot JSON has only the note text, so clients guess from (stop_codon - start_codon) % 3.
  • Genetic code. RefSeq writes transl_table=2 on the mitochondrial CDS rows. Clients hardcode the mitochondrial contigs instead.

Changes

  • gff_parser.py
    • Reads transl_except and transl_table from CDS rows, next to the existing Note handling. This covers RefSeq GFF3, and GTF files that carry these attributes.
    • GTFParser reads Ensembl Selenocysteine rows as Sec.
    • Once the exons and the CDS are known, each codon is converted to its 1-based codon number within the CDS. The conversion uses the first base of the codon in transcript direction and the existing _get_transcript_position, so alignment gaps and coordinate holes (start_codon/stop_codon: documented 1-based, and undefined across an inter-exon transcript gap #123) are handled as for start_codon. A codon that is not a codon of the CDS is skipped with a warning.
  • cdot/models.py: Transcript has the optional fields transl_except and transl_table. docs/json_data_format.md and docs/cdot-json-schema.json are regenerated.
  • JSON_SCHEMA_VERSION is 0.2.35. Both changelogs have an entry.

JSON

"NM_080430.4": {"start_codon": 63, "stop_codon": 501, "transl_except": {"Sec": [48]}, ...},
"fake-rna-ND1": {"start_codon": 0, "stop_codon": 956, "transl_except": {"TERM": [319]}, "transl_table": 2, ...}
  • Both keys sit at the transcript level, next to start_codon and stop_codon. They describe the transcript and its protein, not the alignment to one build.
  • transl_except maps an amino acid to codon numbers, i.e. positions in the protein. Codon numbers do not depend on the genome build. They match HGVS p. numbering, the GENCODE IDs such as selenocysteine:ENST00000400518.5:493, and the RefSeq protein.
  • The amino acid names are those of RefSeq transl_except: Sec, TERM, Met, Other and so on. All of them are kept, not only Sec and TERM. The RefSeq GRCh38 annotation RS_2025_08 has 109 distinct transl_except values with 14 amino acid names.
  • transl_table is written only when the source names one. Without it, the code is the standard one or unknown. Ensembl GTF has no such attribute.

Compatibility

Both keys are optional and absent for transcripts without such data. The models structs use forbid_unknown_fields=False, so older clients ignore the keys. Clients check only major.minor of the data schema, so a patch bump is enough, as for ccds and transcript_support_level in 0.2.33.

Tests

  • test_refseq_gff3_selenocysteine: SELENOM from RefSeq annotation release 110 (minus strand, selenocysteine codon in exon 2) gives {"Sec": [48]} and no transl_table.
  • test_ensembl_gtf_selenocysteine: SELENOM from the Ensembl 108 GTF gives {"Sec": [48]}. The Selenocysteine row leaves the exons and the CDS unchanged.
  • test_mito_transl_except_and_transl_table: all 13 RefSeq mitochondrial transcripts get transl_table 2. ND1 (CDS of 956 bases) gets {"TERM": [319]}, ATP8 gets no transl_except.
  • test_transcript_translation_fields: the model keeps both fields when present and gives None when absent.
  • python -m pytest tests/: 416 passed, 5 skipped. Before: 412 passed, 5 skipped.

Checked on real subsets, outside the tests:

  • Ensembl 108 GTF, all transcripts of TXNRD2, SELENOO and SELENOM: the 12 positions equal those in the GENCODE 40 stop_codon_redefined_as_selenocysteine IDs, e.g. ENST00000400518.5 at 493.
  • RefSeq 110, the same genes: 6 transcripts, e.g. NM_001352301.2 at 493 and NM_031454.2 at 667, in line with Ensembl.
  • RefSeq 110 TMEM247 NM_001145051.2, CDS of 659 bases: {"TERM": [220]}.

Not included

  • GENCODE GFF3 stop_codon_redefined_as_selenocysteine rows. GFF3Parser cannot read GENCODE GFF3 today: it stops at the first start_codon row, whose parent is a transcript. cdot builds its Ensembl data from GTF, which has the Selenocysteine rows.
  • Ensembl GFF3 has no selenocysteine rows.

Closes #131

New optional transcript keys, data schema 0.2.35:

- transl_except: amino acid -> codon numbers in the CDS (1-based, ie
  protein positions), eg {"Sec": [48]}. From RefSeq transl_except on
  the CDS rows and from Ensembl GTF Selenocysteine rows. RefSeq stop
  codons completed by the poly(A) tail are "TERM".
- transl_table: the RefSeq genetic code of the CDS, eg 2 on chrMT.

A consumer can then read UGA as selenocysteine only at the annotated
codons, instead of at every UGA of a selenoprotein.
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.

Keep transl_except and transl_table in the JSON

1 participant