CoolFace
Apppublic

Tsimech2000/Codon_Translator

sourceHugging Facemitupdated 2mo agoView on Hugging Face
0likes
translation.py202 linesDownload Raw Back to root
1"""Sequence validation and codon translation utilities.2 3Internally, sequences are represented as DNA (T rather than U).  This keeps4reverse-complement and genetic-code logic consistent while accepting either5DNA or RNA from the user interface.6"""7 8from __future__ import annotations9 10from dataclasses import dataclass11from itertools import product12import re13 14 15IUPAC_BASES = {16    "A": "A", "C": "C", "G": "G", "T": "T", "U": "T",17    "R": "AG", "Y": "CT", "S": "GC", "W": "AT", "K": "GT",18    "M": "AC", "B": "CGT", "D": "AGT", "H": "ACT", "V": "ACG",19    "N": "ACGT",20}21 22COMPLEMENT = str.maketrans(23    "ACGTRYSWKMBDHVN", "TGCAYRSWMKVHDBN"24)25 26AA_NAMES = {27    "A": "Alanine", "R": "Arginine", "N": "Asparagine",28    "D": "Aspartic acid", "C": "Cysteine", "E": "Glutamic acid",29    "Q": "Glutamine", "G": "Glycine", "H": "Histidine",30    "I": "Isoleucine", "L": "Leucine", "K": "Lysine",31    "M": "Methionine", "F": "Phenylalanine", "P": "Proline",32    "S": "Serine", "T": "Threonine", "W": "Tryptophan",33    "Y": "Tyrosine", "V": "Valine", "*": "Stop", "X": "Ambiguous",34}35 36# Average residue masses (Da); a water molecule is added for an intact peptide.37AA_RESIDUE_MASS = {38    "A": 71.0788, "R": 156.1875, "N": 114.1038, "D": 115.0886,39    "C": 103.1388, "E": 129.1155, "Q": 128.1307, "G": 57.0519,40    "H": 137.1411, "I": 113.1594, "L": 113.1594, "K": 128.1741,41    "M": 131.1926, "F": 147.1766, "P": 97.1167, "S": 87.0782,42    "T": 101.1051, "W": 186.2132, "Y": 163.1760, "V": 99.1326,43}44 45_BASES = "TCAG"46_AA_STRING = "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG"47_STANDARD = {48    "".join(codon): aa49    for codon, aa in zip(product(_BASES, repeat=3), _AA_STRING)50}51 52GENETIC_CODES = {53    "Standard": {54        "table": _STANDARD,55        "starts": {"ATG"},56    },57    "Vertebrate mitochondrial": {58        "table": {**_STANDARD, "ATA": "M", "TGA": "W", "AGA": "*", "AGG": "*"},59        "starts": {"ATT", "ATC", "ATA", "ATG", "GTG"},60    },61}62 63AMINO_ACID_SMILES = {64    "A": "N[C@@H](C)C(=O)O",65    "R": "N[C@@H](CCCNC(=N)N)C(=O)O",66    "N": "N[C@@H](CC(=O)N)C(=O)O",67    "D": "N[C@@H](CC(=O)O)C(=O)O",68    "C": "N[C@@H](CS)C(=O)O",69    "E": "N[C@@H](CCC(=O)O)C(=O)O",70    "Q": "N[C@@H](CCC(=O)N)C(=O)O",71    "G": "NCC(=O)O",72    "H": "N[C@@H](Cc1c[nH]cn1)C(=O)O",73    "I": "N[C@@H]([C@@H](C)CC)C(=O)O",74    "L": "N[C@@H](CC(C)C)C(=O)O",75    "K": "N[C@@H](CCCCN)C(=O)O",76    "M": "N[C@@H](CCSC)C(=O)O",77    "F": "N[C@@H](Cc1ccccc1)C(=O)O",78    "P": "O=C(O)[C@@H]1CCCN1",79    "S": "N[C@@H](CO)C(=O)O",80    "T": "N[C@@H]([C@H](O)C)C(=O)O",81    "W": "N[C@@H](Cc1c[nH]c2ccccc12)C(=O)O",82    "Y": "N[C@@H](Cc1ccc(O)cc1)C(=O)O",83    "V": "N[C@@H](C(C)C)C(=O)O",84}85 86 87@dataclass(frozen=True)88class TranslationResult:89    protein: str90    rows: list[dict]91    warnings: list[str]92    oriented_sequence: str93 94 95def normalize_sequence(raw: str) -> tuple[str, list[str]]:96    """Return an uppercase DNA sequence and any invalid characters.97 98    FASTA headers, whitespace, digits, and common position punctuation are99    ignored.  Other characters are reported rather than silently discarded.100    """101    lines = [line for line in (raw or "").splitlines() if not line.lstrip().startswith(">")]102    joined = "".join(lines).upper()103    ignored = set(" \t\r\n0123456789-._")104    invalid = sorted({char for char in joined if char not in IUPAC_BASES and char not in ignored})105    sequence = "".join(IUPAC_BASES[char][0] if char == "U" else char for char in joined if char in IUPAC_BASES)106    return sequence.replace("U", "T"), invalid107 108 109def reverse_complement(sequence: str) -> str:110    return sequence.translate(COMPLEMENT)[::-1]111 112 113def resolve_codon(codon: str, table: dict[str, str]) -> str:114    """Translate an IUPAC codon; return X when its meanings disagree."""115    if len(codon) != 3:116        return "X"117    possibilities = product(*(IUPAC_BASES[base] for base in codon))118    meanings = {table["".join(parts)] for parts in possibilities}119    return next(iter(meanings)) if len(meanings) == 1 else "X"120 121 122def translate_sequence(123    sequence: str,124    frame: int = 1,125    code_name: str = "Standard",126    mode: str = "Selected frame",127    stop_at_first: bool = True,128) -> TranslationResult:129    if frame not in {-3, -2, -1, 1, 2, 3}:130        raise ValueError("Frame must be one of +1, +2, +3, -1, -2, or -3.")131    if code_name not in GENETIC_CODES:132        raise ValueError(f"Unknown genetic code: {code_name}")133 134    code = GENETIC_CODES[code_name]135    oriented = sequence if frame > 0 else reverse_complement(sequence)136    offset = abs(frame) - 1137    warnings: list[str] = []138 139    start_offset = offset140    if mode == "First start-to-stop ORF":141        found = None142        for position in range(offset, len(oriented) - 2, 3):143            if oriented[position:position + 3] in code["starts"]:144                found = position145                break146        if found is None:147            return TranslationResult("", [], ["No in-frame start codon was found."], oriented)148        start_offset = found149 150    rows: list[dict] = []151    protein: list[str] = []152    found_stop = False153    for position in range(start_offset, len(oriented) - 2, 3):154        codon = oriented[position:position + 3]155        aa = resolve_codon(codon, code["table"])156        # Alternative initiation codons encode methionine only when they are157        # actually used to initiate an ORF.158        if mode == "First start-to-stop ORF" and position == start_offset and codon in code["starts"]:159            aa = "M"160        rows.append({161            "Nucleotide position": position + 1,162            "Codon (DNA)": codon,163            "Codon (mRNA)": codon.replace("T", "U"),164            "1-letter": aa,165            "Amino acid": AA_NAMES[aa],166        })167        if aa == "*":168            found_stop = True169            if stop_at_first or mode == "First start-to-stop ORF":170                break171            protein.append("*")172        else:173            protein.append(aa)174 175    translated_nt = max(0, len(oriented) - start_offset)176    remainder = translated_nt % 3177    if remainder:178        warnings.append(f"{remainder} trailing nucleotide(s) do not form a complete codon.")179    if any(row["1-letter"] == "X" for row in rows):180        warnings.append("At least one ambiguous codon has multiple possible amino-acid meanings and is shown as X.")181    if mode == "First start-to-stop ORF" and not found_stop:182        warnings.append("A start codon was found, but no in-frame stop codon occurs before the sequence ends.")183    return TranslationResult("".join(protein), rows, warnings, oriented)184 185 186def gc_percent(sequence: str) -> float | None:187    canonical = [base for base in sequence if base in "ACGT"]188    if not canonical:189        return None190    return 100.0 * sum(base in "GC" for base in canonical) / len(canonical)191 192 193def peptide_mass(protein: str) -> float | None:194    if not protein or any(aa not in AA_RESIDUE_MASS for aa in protein):195        return None196    return 18.0153 + sum(AA_RESIDUE_MASS[aa] for aa in protein)197 198 199def safe_filename(value: str) -> str:200    cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip())201    return cleaned.strip("._") or "translation"202