From ff0e58181982ffc56aecd17a460a684bf9f59a22 Mon Sep 17 00:00:00 2001 From: Jason Stajich Date: Sun, 4 Oct 2026 08:51:11 -0700 Subject: [PATCH] detect: skip exonerate for genetic codes it does not have (v0.6.1) exonerate 2.4.0 has NCBI tables 1-6, 9-16 and 21-23. For code 26 (Alaninales, CUG = Ala) it exits 1, and _run_checked made that fatal for the whole genome: 20 of 19,415 BFD Ascomycota genomes got no report in the v0.6.0 run. Exonerate is now skipped for such codes; the gene model comes from miniprot alone (polished_single). miniprot 0.18 -T 26 translates CTG as Ala (synthetic gene, 15 CTG in 120 aa: identity 1.00 with -T 26, 0.875 with -T 1 or -T 12). Version 0.6.1 (pyproject, CITATION.cff, README, CHANGELOG). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JkhYFEPaa1WenE5MtA8Qya --- CHANGELOG.md | 10 +++++- CITATION.cff | 4 +-- README.md | 6 ++-- pyproject.toml | 2 +- src/MATPredict/detect/search.py | 31 ++++++++++++++++++ tests/detect/test_genetic_code.py | 54 +++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae0f820..70267c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,15 @@ All notable changes to MATPredict. Versions follow [semantic versioning](https:/ release tags may carry a clade suffix naming the lineage whose infrastructure that release completed. -## [Unreleased] +## [0.6.1] — 2026-10-04 — `v0.6.1` + +### Fixed +- Genomes that use an NCBI genetic code exonerate does not have no longer fail. + exonerate 2.4.0 has tables 1-6, 9-16 and 21-23; for any other code it exited 1 + and the whole genome got no report. Code 26 (Alaninales: *Pachysolen*, + *Nakazawaea*; CUG = Ala) failed 20 of the 19,415 BFD Ascomycota genomes in the + v0.6.0 run. Exonerate is now skipped for such codes and gene models come from + miniprot alone (`polished_single`), which translates code 26 correctly. ### Changed - No default NCBI e-mail. The address comes from `$MATPREDICT_NCBI_EMAIL` or the diff --git a/CITATION.cff b/CITATION.cff index 4b9ca9fc..04f4aaae 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,7 +8,7 @@ authors: affiliation: University of California-Riverside orcid: "https://orcid.org/0000-0002-7591-0020" title: "MATPredict: MAT locus identification in Fungi" -version: 0.6.0 +version: 0.6.1 doi: TBD -date-released: 2026-10-03 +date-released: 2026-10-04 url: "https://github.com/stajichlab/MATPredict" diff --git a/README.md b/README.md index 03d7a5f2..7aaebe6f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ idiomorph or mating type it is (for example Plus/Minus, MAT1-1/MAT1-2, HD/PR alleles). Every reference record traces to a publication or a validated deposit, and every call reports the evidence behind it. -Current release: [`v0.6.0`](https://github.com/stajichlab/MATPredict/releases/tag/v0.6.0). +Current release: [`v0.6.1`](https://github.com/stajichlab/MATPredict/releases/tag/v0.6.1). Changes: [CHANGELOG.md](CHANGELOG.md). ## Contents @@ -385,14 +385,14 @@ No paper describes MATPredict yet. Cite the software and the release you used. The citation metadata is in [CITATION.cff](CITATION.cff); GitHub shows it under "Cite this repository". -> Stajich JE. MATPredict: MAT locus identification in Fungi. Version 0.6.0. +> Stajich JE. MATPredict: MAT locus identification in Fungi. Version 0.6.1. > https://github.com/stajichlab/MATPredict ```bibtex @software{matpredict, author = {Stajich, Jason E.}, title = {MATPredict: MAT locus identification in Fungi}, - version = {0.6.0}, + version = {0.6.1}, url = {https://github.com/stajichlab/MATPredict}, year = {2026} } diff --git a/pyproject.toml b/pyproject.toml index c5d0ad3a..7789d49e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "MATPredict" -version = "0.6.0" +version = "0.6.1" description = "Fungal MAT locus annotator/classifier" authors = [{name = "Jason Stajich", email = "jason.stajich@ucr.edu"}] requires-python = ">=3.11" diff --git a/src/MATPredict/detect/search.py b/src/MATPredict/detect/search.py index 0d3dcf8c..6a0f6077 100644 --- a/src/MATPredict/detect/search.py +++ b/src/MATPredict/detect/search.py @@ -358,6 +358,23 @@ def search_fast_path( #: single HSP with it and sum only across HSPs already grouped into one gene. +#: NCBI translation tables built into exonerate 2.4.0. Measured 2026-10-04 by +#: running `exonerate --geneticcode N` for every NCBI table: 1-6, 9-16 and +#: 21-23 run; 24 and higher exit 1 with "No built in genetic code +#: corresponding to id [N]". Table 26 (Alternative Yeast Nuclear, CUG = Ala, +#: the Alaninales: Pachysolen, Nakazawaea) made every such genome fail in the +#: v0.6.0 Ascomycota run (20 of 19,415 BFD genomes). miniprot 0.18 has table 26 +#: (a synthetic gene with 15 CTG codons in 120 aa aligns at 100% identity with +#: `-T 26`, 87.5% with `-T 1` or `-T 12`). +EXONERATE_GENETIC_CODES = frozenset({1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 16, 21, 22, 23}) +_warned_exonerate_codes: set[int] = set() + + +def exonerate_supports_code(genetic_code: int | None) -> bool: + """True when exonerate can translate with this code (None = table 1).""" + return genetic_code is None or int(genetic_code) in EXONERATE_GENETIC_CODES + + def _gencode_args(genetic_code: int | None, flag: str) -> list[str]: """`[flag, str(code)]`, or `[]` for the standard code / unknown. @@ -638,6 +655,20 @@ def polish_with_exonerate( roles_by_family = _roles_by_family([family]) contig, win_start, _win_end = window + # exonerate has no table for some NCBI codes (EXONERATE_GENETIC_CODES). + # Running it anyway exits 1, which `_run_checked` makes fatal for the whole + # genome. Translating with a different table would give wrong residues. + # So skip exonerate: the caller's `classify` then records a + # `polished_single` model on miniprot alone, as for `polish: miniprot` genes. + if not exonerate_supports_code(genetic_code): + if int(genetic_code) not in _warned_exonerate_codes: + _warned_exonerate_codes.add(int(genetic_code)) + logger.warning( + "exonerate has no genetic code %d; gene models use miniprot only", + int(genetic_code), + ) + return None + with tempfile.TemporaryDirectory() as tmp_dir_name: tmp_dir = Path(tmp_dir_name) target_fasta = _extract_window(genome_fasta, window, tmp_dir) diff --git a/tests/detect/test_genetic_code.py b/tests/detect/test_genetic_code.py index aaa92734..92681c75 100644 --- a/tests/detect/test_genetic_code.py +++ b/tests/detect/test_genetic_code.py @@ -116,3 +116,57 @@ def test_tblastn_omits_the_flag_for_the_standard_code(tmp_path): search_localize(tmp_path / "g.fa", [fam], tmp_path / "r.faa", {"r": key}, runner=_runner(cmds), genetic_code=1) assert "-db_gencode" not in next(c for c in cmds if c[0] == "tblastn") + + +def _exonerate_args(tmp_path, code, cmds): + from MATPredict.detect.family_registry import Family, FamilyKey + from MATPredict.detect.search import polish_with_exonerate + key = FamilyKey("P", "MAT") + fam = Family(key=key, vocabulary_type="enum", idiomorph_values=["a"], + idiomorph_pattern=None, + genes=[{"name": "g1", "role": "core_MAT"}], taxonomic_scope=[1]) + (tmp_path / "g.fa").write_text(">c1\n" + "ACGT" * 100 + "\n") + return polish_with_exonerate(tmp_path / "g.fa", fam, "g1", tmp_path / "r.faa", {"r": key}, + ("c1", 1, 400), runner=_runner(cmds), genetic_code=code) + + +def test_exonerate_is_skipped_for_a_code_it_does_not_have(tmp_path): + """Table 26 (Alaninales, CUG = Ala) is not built into exonerate 2.4.0. Run + anyway, it exits 1 and the whole genome failed (20 BFD genomes in the v0.6.0 + Ascomycota run). Now exonerate is not run and the gene is left to miniprot.""" + cmds = [] + assert _exonerate_args(tmp_path, 26, cmds) is None + assert not any(c[0] == "exonerate" for c in cmds) + + +@pytest.mark.parametrize("code", [1, 12, None]) +def test_exonerate_still_runs_for_codes_it_has(tmp_path, code): + cmds = [] + _exonerate_args(tmp_path, code, cmds) + exo = next(c for c in cmds if c[0] == "exonerate") + if code in (None, 1): + assert "--geneticcode" not in exo + else: + assert exo[exo.index("--geneticcode") + 1] == str(code) + + +def test_miniprot_is_given_code_26(tmp_path): + from MATPredict.detect.family_registry import Family, FamilyKey + from MATPredict.detect.search import polish_with_miniprot + key = FamilyKey("P", "MAT") + fam = Family(key=key, vocabulary_type="enum", idiomorph_values=["a"], + idiomorph_pattern=None, + genes=[{"name": "g1", "role": "core_MAT"}], taxonomic_scope=[1]) + (tmp_path / "g.fa").write_text(">c1\n" + "ACGT" * 100 + "\n") + cmds = [] + polish_with_miniprot(tmp_path / "g.fa", fam, "g1", tmp_path / "r.faa", {"r": key}, + ("c1", 1, 400), runner=_runner(cmds), genetic_code=26) + mp = next(c for c in cmds if c[0] == "miniprot") + assert mp[mp.index("-T") + 1] == "26" + + +def test_the_exonerate_code_table_matches_exonerate_2_4_0(): + from MATPredict.detect.search import EXONERATE_GENETIC_CODES, exonerate_supports_code + assert EXONERATE_GENETIC_CODES == {1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 16, 21, 22, 23} + assert exonerate_supports_code(None) and exonerate_supports_code(12) + assert not exonerate_supports_code(26)