Spaces:
Running
Running
Download bioai-platform/backend/app/tools/motif_scanner.py from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/backend/app/tools/motif_scanner.py
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/bioai-platform/backend/app/tools/motif_scanner.py
-
curl -L -o motif_scanner.py https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/backend/app/tools/motif_scanner.py
12.9 kB
| """Local protein motif scanner. | |
| Implements the two scanning modes a bench biologist uses to find short | |
| functional motifs in a protein: | |
| * Custom PROSITE patterns (e.g. ``[ST]-x-[RK]``, ``N-{P}-[ST]-{P}``) parsed | |
| into a Python regular expression and matched against the query. | |
| * A curated library of well-known eukaryotic motif patterns (glycosylation, | |
| phosphorylation, myristoylation, zinc fingers, the P-loop, ...). | |
| Pure local computation — no network calls. This is the "scan your sequence | |
| for motifs" workflow that PROSITE/ScanProsite expose as a web service. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from app.services.sequence_utils import detect_sequence_type | |
| class MotifError(ValueError): | |
| pass | |
| # Well-known PROSITE-style patterns, organized by biological category. | |
| # accession — PROSITE entry (PSxxxxx) when the pattern maps to a real | |
| # curated PROSITE profile; "" for consensus motifs that are | |
| # commonly used but not part of the PROSITE database. | |
| # specificity — "high" for rare, low-false-positive patterns (zinc fingers, | |
| # P-loop, active sites); "loose" for short, high-frequency | |
| # consensus sites (most phosphorylation/glycosylation sites). | |
| # category — groups patterns for the UI: PTM / DNA binding / RNA binding | |
| # / Catalytic / Signal & targeting / Structural. | |
| MOTIF_LIBRARY: dict[str, dict[str, str]] = { | |
| # --- Post-translational modifications -------------------------------- | |
| "N-glycosylation site": { | |
| "accession": "PS00001", | |
| "category": "PTM", | |
| "specificity": "high", | |
| "description": "Asn-linked glycosylation: N-{P}-[ST]-{P}", | |
| "pattern": "N-{P}-[ST]-{P}", | |
| }, | |
| "Tyrosine sulfation site": { | |
| "accession": "PS00003", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "Tyrosine sulfation consensus: [ED]-x(2)-[DE]-x(2)-Y", | |
| "pattern": "[ED]-x(2)-[DE]-x(2)-Y", | |
| }, | |
| "cAMP/cGMP kinase site": { | |
| "accession": "PS00004", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "cAMP/cGMP-dependent protein kinase phosphorylation: [RK]-{2}-x-[ST]", | |
| "pattern": "[RK]-{2}-x-[ST]", | |
| }, | |
| "Protein kinase C site": { | |
| "accession": "PS00005", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "Protein kinase C phosphorylation: [ST]-x-[RK]", | |
| "pattern": "[ST]-x-[RK]", | |
| }, | |
| "Casein kinase II site": { | |
| "accession": "PS00006", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "Casein kinase II phosphorylation: [ST]-x(2)-[DE]", | |
| "pattern": "[ST]-x(2)-[DE]", | |
| }, | |
| "Tyrosine kinase site": { | |
| "accession": "PS00007", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "Tyrosine kinase phosphorylation: [RK]-x(2,3)-[DE]-x(2,3)-Y", | |
| "pattern": "[RK]-x(2,3)-[DE]-x(2,3)-Y", | |
| }, | |
| "N-myristoylation site": { | |
| "accession": "PS00008", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "Myristyl anchor: G-{EDRKHPFYW}-x(2)-[STAGCN]-{P}", | |
| "pattern": "G-{EDRKHPFYW}-x(2)-[STAGCN]-{P}", | |
| }, | |
| "Amidation site": { | |
| "accession": "PS00009", | |
| "category": "PTM", | |
| "specificity": "loose", | |
| "description": "C-terminal amidation signal: x-G-[RK]-x", | |
| "pattern": "x-G-[RK]-x", | |
| }, | |
| # --- DNA binding ------------------------------------------------------ | |
| "C2H2 zinc finger": { | |
| "accession": "PS00028", | |
| "category": "DNA binding", | |
| "specificity": "high", | |
| "description": "Classic zinc finger half: C-x(2,4)-C-x(3)-[LIVMFYWC]-x(8)-H-x(3,5)-H", | |
| "pattern": "C-x(2,4)-C-x(3)-[LIVMFYWC]-x(8)-H-x(3,5)-H", | |
| }, | |
| "Leucine zipper": { | |
| "accession": "PS00029", | |
| "category": "DNA binding", | |
| "specificity": "loose", | |
| "description": "Heptad repeat dimerization: L-x(6)-L-x(6)-L-x(6)-L", | |
| "pattern": "L-x(6)-L-x(6)-L-x(6)-L", | |
| }, | |
| "Nuclear hormone receptor C4 finger": { | |
| "accession": "PS00030", | |
| "category": "DNA binding", | |
| "specificity": "high", | |
| "description": "C4 zinc finger of steroid hormone receptors: C-x(2)-C-x(13)-C-x(2)-C", | |
| "pattern": "C-x(2)-C-x(13)-C-x(2)-C", | |
| }, | |
| # --- RNA binding ------------------------------------------------------ | |
| "RNP-1 RNA-binding region": { | |
| "accession": "PS00031", | |
| "category": "RNA binding", | |
| "specificity": "high", | |
| "description": "RNP-1 motif of RNA-recognition proteins: [RK]-G-{P}-[FY]-{V}-x-[FYWH]-[LIVMFY]", | |
| "pattern": "[RK]-G-{P}-[FY]-{V}-x-[FYWH]-[LIVMFY]", | |
| }, | |
| # --- Catalytic / nucleotide binding ---------------------------------- | |
| "ATP/GTP-binding P-loop": { | |
| "accession": "PS00017", | |
| "category": "Catalytic", | |
| "specificity": "high", | |
| "description": "Walker A / P-loop: [AG]-x(4)-G-K-[ST]", | |
| "pattern": "[AG]-x(4)-G-K-[ST]", | |
| }, | |
| "Protein kinase ATP-binding": { | |
| "accession": "PS00107", | |
| "category": "Catalytic", | |
| "specificity": "high", | |
| "description": "Protein kinase ATP-binding signature: [LIVMFYC]-x-[HY]-x-D-[LIVMFY]-K-x(2)-N-[LIVMFYCT]", | |
| "pattern": "[LIVMFYC]-x-[HY]-x-D-[LIVMFY]-K-x(2)-N-[LIVMFYCT]", | |
| }, | |
| "Serine protease active site": { | |
| "accession": "PS00138", | |
| "category": "Catalytic", | |
| "specificity": "high", | |
| "description": "Chymotrypsin-family catalytic Ser: [LIVMFYWC]-G-[HYW]-S-[LIVMFYWC]-G-x(2)-[SAG]", | |
| "pattern": "[LIVMFYWC]-G-[HYW]-S-[LIVMFYWC]-G-x(2)-[SAG]", | |
| }, | |
| "Aspartic protease active site": { | |
| "accession": "PS00141", | |
| "category": "Catalytic", | |
| "specificity": "high", | |
| "description": "Aspartic protease catalytic Asp: [LIVMFGAC]-[LIVMTADN]-[LIVFSA]-D-[STAG]-G-[STAV]-[STAPDENQ]-x(2)-[LIVMFSTNC]-x-[LIVMFGTA]", | |
| "pattern": "[LIVMFGAC]-[LIVMTADN]-[LIVFSA]-D-[STAG]-G-[STAV]-[STAPDENQ]-x(2)-[LIVMFSTNC]-x-[LIVMFGTA]", | |
| }, | |
| # --- Signal & targeting ----------------------------------------------- | |
| "Nuclear localization signal (NLS)": { | |
| "accession": "", | |
| "category": "Signal & targeting", | |
| "specificity": "loose", | |
| "description": "Monopartite NLS consensus: K-[RK]-x(2)-[KRA]", | |
| "pattern": "K-[RK]-x(2)-[KRA]", | |
| }, | |
| "ER retention signal": { | |
| "accession": "", | |
| "category": "Signal & targeting", | |
| "specificity": "loose", | |
| "description": "C-terminal endoplasmic-reticulum retention: [KRHQSA]-[DENQ]-E-L>", | |
| "pattern": "[KRHQSA]-[DENQ]-E-L>", | |
| }, | |
| "Secretory signal peptide": { | |
| "accession": "", | |
| "category": "Signal & targeting", | |
| "specificity": "loose", | |
| "description": "Hydrophobic N-terminal core of a cleavable signal peptide: M-x-[LIVMF]-x(3)-[LIVMF]", | |
| "pattern": "M-x-[LIVMF]-x(3)-[LIVMF]", | |
| }, | |
| # --- Structural / adhesion -------------------------------------------- | |
| "RGD cell-attachment": { | |
| "accession": "PS00016", | |
| "category": "Structural", | |
| "specificity": "high", | |
| "description": "Integrin-binding tripeptide: R-G-D", | |
| "pattern": "R-G-D", | |
| }, | |
| } | |
| CATEGORY_ORDER = ["PTM", "DNA binding", "RNA binding", "Catalytic", "Signal & targeting", "Structural"] | |
| def list_motif_categories() -> list[str]: | |
| """Return the ordered list of pattern categories present in the library.""" | |
| return [c for c in CATEGORY_ORDER if any(e["category"] == c for e in MOTIF_LIBRARY.values())] | |
| def get_motif_patterns() -> list[dict]: | |
| """Return the full library as a list (for presets / API listings).""" | |
| return [ | |
| { | |
| "name": name, | |
| "accession": entry["accession"], | |
| "category": entry["category"], | |
| "specificity": entry["specificity"], | |
| "description": entry["description"], | |
| "pattern": entry["pattern"], | |
| } | |
| for name, entry in MOTIF_LIBRARY.items() | |
| ] | |
| def prosite_to_regex(pattern: str) -> str: | |
| """Translate a PROSITE pattern string into a Python regular expression. | |
| Supported syntax: ``-`` (separator, ignored), ``x`` (any residue), | |
| ``[AC]`` (one of), ``{ED}`` (none of), ``x(4)`` / ``x(2,4)`` | |
| (repetition), ``<`` (N-terminus) and ``>`` (C-terminus). | |
| """ | |
| pattern = (pattern or "").strip() | |
| if not pattern: | |
| raise MotifError("Pattern is empty") | |
| i = 0 | |
| n = len(pattern) | |
| out: list[str] = [] | |
| def consume_repeat() -> None: | |
| nonlocal i | |
| if i < n and pattern[i] == "(": | |
| j = pattern.find(")", i + 1) | |
| if j == -1: | |
| raise MotifError(f"Unterminated repeat quantifier at position {i}") | |
| quant = pattern[i + 1:j] | |
| if not re.fullmatch(r"\d+(,\d+)?", quant): | |
| raise MotifError(f"Invalid repeat quantifier ({quant}) at position {i}") | |
| out.append("{" + quant + "}") | |
| i = j + 1 | |
| while i < n: | |
| c = pattern[i] | |
| if c == "-": | |
| i += 1 | |
| elif c == "<": | |
| out.append("^") | |
| i += 1 | |
| elif c == ">": | |
| out.append("$") | |
| i += 1 | |
| elif c == "[": | |
| j = pattern.find("]", i + 1) | |
| if j == -1: | |
| raise MotifError(f"Unterminated '[' group at position {i}") | |
| group = pattern[i + 1:j] | |
| if not group or any(ch not in "ACDEFGHIKLMNPQRSTVWY" for ch in group): | |
| raise MotifError(f"Invalid residue group [{group}] at position {i}") | |
| out.append("[" + group + "]") | |
| i = j + 1 | |
| consume_repeat() | |
| elif c == "{": | |
| j = pattern.find("}", i + 1) | |
| if j == -1: | |
| raise MotifError(f"Unterminated '{{' group at position {i}") | |
| group = pattern[i + 1:j] | |
| if not group or any(ch not in "ACDEFGHIKLMNPQRSTVWY" for ch in group): | |
| raise MotifError(f"Invalid exclusion group {{{group}}} at position {i}") | |
| out.append("[^" + group + "]") | |
| i = j + 1 | |
| consume_repeat() | |
| elif c in ("x", "X"): | |
| out.append("[A-Z]") | |
| i += 1 | |
| consume_repeat() | |
| elif c in "ACDEFGHIKLMNPQRSTVWY": | |
| out.append(c) | |
| i += 1 | |
| consume_repeat() | |
| else: | |
| raise MotifError(f"Unsupported symbol {c!r} at position {i}") | |
| return "".join(out) | |
| def _clean_protein(seq: str) -> str: | |
| return "".join(ch for ch in seq.upper() if ch.isalpha()) | |
| def scan_pattern(sequence: str, pattern: str) -> dict: | |
| """Scan a protein for a single PROSITE pattern.""" | |
| seq = _clean_protein(sequence) | |
| if not seq: | |
| raise MotifError("Sequence is empty") | |
| regex_src = prosite_to_regex(pattern) | |
| try: | |
| regex = re.compile(regex_src) | |
| except re.error as e: | |
| raise MotifError(f"Pattern compiles to an invalid regular expression: {e}") | |
| matches = [] | |
| for m in regex.finditer(seq): | |
| matches.append({ | |
| "start": m.start() + 1, | |
| "end": m.end(), | |
| "motif": m.group(0), | |
| }) | |
| return { | |
| "sequence_type": detect_sequence_type(seq), | |
| "pattern": pattern, | |
| "regex": regex_src, | |
| "count": len(matches), | |
| "matches": matches, | |
| } | |
| def scan_library(sequence: str, categories: list[str] | None = None) -> dict: | |
| """Scan a protein against the curated PROSITE library. | |
| ``categories`` optionally restricts the scan to a subset of categories | |
| (e.g. ``["PTM", "DNA binding"]``). Unknown category names are ignored. | |
| """ | |
| seq = _clean_protein(sequence) | |
| if not seq: | |
| raise MotifError("Sequence is empty") | |
| allowed = set(categories or []) | |
| scanned = 0 | |
| hits = [] | |
| for name, entry in MOTIF_LIBRARY.items(): | |
| if allowed and entry["category"] not in allowed: | |
| continue | |
| scanned += 1 | |
| try: | |
| result = scan_pattern(seq, entry["pattern"]) | |
| except MotifError: | |
| continue | |
| if result["count"] > 0: | |
| hits.append({ | |
| "name": name, | |
| "accession": entry["accession"], | |
| "category": entry["category"], | |
| "specificity": entry["specificity"], | |
| "description": entry["description"], | |
| "pattern": entry["pattern"], | |
| "count": result["count"], | |
| "matches": result["matches"], | |
| }) | |
| hits.sort(key=lambda h: (-h["count"], h["name"])) | |
| return { | |
| "sequence_type": detect_sequence_type(seq), | |
| "length": len(seq), | |
| "patterns_scanned": scanned, | |
| "motifs_found": len(hits), | |
| "hits": hits, | |
| } | |