Spaces:
Running
Running
File size: 12,881 Bytes
60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 033540e 60aee05 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | """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,
}
|