Source code for atomworks.io.tools.fasta

"""Convenience utils for working with (generalized) FASTA files."""

import logging
import os
import re
from collections import Counter

from atomworks.constants import (
    CCD_MIRROR_PATH,
    STANDARD_AA_ONE_LETTER,
    STANDARD_DNA,
    STANDARD_DNA_ONE_LETTER,
    STANDARD_RNA,
)
from atomworks.enums import ChainType
from atomworks.io.utils.ccd import (
    check_ccd_codes_are_available,
)
from atomworks.io.utils.sequence import get_3_from_1_letter_code

logger = logging.getLogger("atomworks.io")


[docs] def split_generalized_fasta_sequence(sequence: str) -> list[str]: """ Splits a sequence at each letter, keeping groups with parentheses intact. Args: - sequence (str): The input sequence to be split. Returns: - List[str]: A list of individual letters and/or groups with parentheses. Example: >>> split_generalized_fasta_sequence("ABC(DEF)GH(IJ)K") ['A', 'B', 'C', '(DEF)', 'G', 'H', '(IJ)', 'K'] """ pattern = r"\([^)]*\)|\w" return re.findall(pattern, sequence)
[docs] def one_letter_to_ccd_code( seq: list[str], chain_type: ChainType, ccd_mirror_path: os.PathLike = CCD_MIRROR_PATH, check_ccd_codes: bool = True ) -> list[str]: """ Convert a sequence of one-letter codes or parenthesized full CCD IDs to full CCD IDs. This function takes a list of either one-letter amino acid codes or parenthesized CCD IDs and converts them to their corresponding full CCD (Chemical Component Dictionary) IDs. It handles both standard amino acids and non-standard chemical components. Args: seq (list[str]): A list of one-letter codes or parenthesized CCD IDs. chain_type (ChainType): The type of chain (e.g., POLYPEPTIDE_L, DNA, RNA) to determine the correct conversion for one-letter codes. check_ccd_codes (bool): If True, check if the CCD IDs are available in the CCD mirror. Returns: - list[str]: A list of full CCD IDs corresponding to the input sequence. Raises: - ValueError: If a non-standard chemical component ID is not found in the processed CCD. Example: >>> seq = ["A", "C", "(SEP)", "G", "H"] >>> chain_type = ChainType.POLYPEPTIDE_L >>> one_letter_to_ccd_code(seq, chain_type) ['ALA', 'CYS', 'SEP', 'GLY', 'HIS'] """ seq_with_ccd_ids = [] for chem_comp_id in seq: if "(" in chem_comp_id: # ... this is a non-standard chemical component that only has a unique # >1 letter code # ... remove the parentheses and yield the 3-letter code chem_comp_id = chem_comp_id.strip("()") # ... ensure it is contained in the CCD mirror if check_ccd_codes: check_ccd_codes_are_available([chem_comp_id], ccd_mirror_path=ccd_mirror_path, mode="raise") else: chem_comp_id = get_3_from_1_letter_code(chem_comp_id, chain_type=chain_type) seq_with_ccd_ids.append(chem_comp_id) return seq_with_ccd_ids
[docs] def infer_chain_type_from_one_letter(seq: str | list[str]) -> ChainType: """Infer chain type from one-letter sequence notation. Supports all common sequence input formats: - Simple one-letter: ``"ACDEFG"`` - Parenthesized notation: ``"(DA)(DT)(DG)"`` (PDB format for DNA/RNA disambiguation) - Mixed with non-canonical amino acids: ``"ACDE(SEP)FG"`` Args: seq: Sequence as string or list (supports all notation types). Returns: Inferred chain type (POLYPEPTIDE_L, DNA, or RNA). Raises: ValueError: If chain type cannot be inferred from the sequence. See Also: :py:func:`~atomworks.io.utils.non_rcsb.infer_chain_type_from_three_letter` - For CCD code arrays from parsed structures. """ # Convert string to list if necessary if isinstance(seq, str): seq = split_generalized_fasta_sequence(seq) # Define one-letter code sets protein_codes = set(STANDARD_AA_ONE_LETTER) dna_codes = set(STANDARD_DNA_ONE_LETTER) rna_codes = set(STANDARD_RNA) hits = Counter() for letter in seq: if letter.startswith("("): # Parenthesized notation - strip and check against CCD codes ccd_code = letter.strip("()") if ccd_code in STANDARD_DNA: # DNA is commonly provided like (DA), (DT), etc. hits["dna"] += 1 elif ccd_code in STANDARD_RNA: # We also support RNA codes like (A), (U), etc; though these are less common hits["rna"] += 1 else: hits["unknown"] += 1 else: # Single-letter code checks if letter in protein_codes: hits["protein"] += 1 if letter in dna_codes: hits["dna"] += 1 if letter in rna_codes: hits["rna"] += 1 # Heuristics: # If the sequence contains more protein hits than DNA or RNA hits, it's probably a protein if hits["protein"] > hits["dna"] and hits["protein"] > hits["rna"]: return ChainType.POLYPEPTIDE_L # Else, if the sequence is all RNA hits, it's probably RNA elif hits["rna"] == len(seq): return ChainType.RNA # Else, if the sequence is all DNA hits, it's probably DNA elif hits["dna"] == len(seq): return ChainType.DNA raise ValueError(f"Could not infer chain type from sequence: {seq=}")