Spaces:
Running
Running
File size: 8,584 Bytes
1bf37a2 | 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 | """The residues you should not mutate, from the people who curated them.
THE PROBLEM
-----------
The engine will happily rank a substitution at the catalytic serine of a
protease as promising. ESM-2 scores how UNUSUAL a residue is, not how load-
bearing it is, and an active-site residue is often perfectly ordinary in
sequence terms β it is the geometry that matters, and a language model over
sequence does not see geometry.
So the single cheapest improvement to design quality is not a better model. It
is asking UniProt what is already known about the positions being mutated:
active sites, binding sites, disulfides, metal ligands, domains. That is
curated, citable, human-reviewed annotation, and it turns "this substitution
looks tolerable" into "this substitution is at the catalytic residue".
WHAT THIS IS
------------
A read of UniProt's feature table, mapped onto positions the user cares about.
No prediction, no score, no model β every item returned carries the evidence
UniProt itself carries. When UniProt has nothing for a protein, that is
reported as "not annotated", never as "safe": absence of annotation is absence
of knowledge, and conflating the two is how a tool talks someone into an
experiment.
Only (accession) leaves the Space. Never a user sequence.
"""
from __future__ import annotations
import json
import re
import urllib.parse
import urllib.request
from typing import Any, Dict, Iterable, List, Optional
_UA = "TuringDNA/1.0 (https://turingdna.com)"
_ENTRY = "https://rest.uniprot.org/uniprotkb"
# Feature types worth interrupting a design for, in the order a designer cares.
# UniProt emits many more (VARIANT, CONFLICT, chains); those describe the
# record, not a reason to avoid a position, and including them would bury the
# signal.
CRITICAL = {
"Active site": "catalytic β mutating this typically abolishes activity",
"Binding site": "contacts the substrate or cofactor",
"Site": "functionally annotated position",
"Metal binding": "coordinates a metal ion the fold depends on",
"Disulfide bond": "forms a disulfide; losing one cysteine breaks both",
"Modified residue": "post-translationally modified",
"Glycosylation": "glycosylation site",
"Cross-link": "covalently cross-linked",
}
CONTEXT = {
"Domain": "domain",
"Region": "region",
"Motif": "motif",
"Repeat": "repeat",
"Transmembrane": "membrane-spanning",
"Signal": "signal peptide",
"Propeptide": "propeptide β cleaved from the mature protein",
}
def _get(url: str, timeout: float = 15.0) -> Optional[dict]:
try:
req = urllib.request.Request(url, headers={"User-Agent": _UA,
"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8"))
except Exception: # noqa: BLE001
return None
def _span(f: Dict[str, Any]) -> Optional[tuple]:
loc = (f.get("location") or {})
try:
s = loc["start"]["value"]
e = loc["end"]["value"]
except (KeyError, TypeError):
return None
if s is None or e is None:
return None
return (int(s), int(e))
def fetch(accession: str) -> Dict[str, Any]:
"""UniProt's curated feature table for one accession."""
acc = re.sub(r"[^A-Za-z0-9\-]", "", accession or "").upper()
if not acc:
return {"ok": False, "error": "No accession given."}
# The whole entry, filtered here. A ?fields= list looked tidier and is
# not valid on the entry endpoint β it returns an error, not a subset, so
# the "tidy" version fetched nothing at all.
data = _get(f"{_ENTRY}/{urllib.parse.quote(acc)}.json")
if not data:
return {"ok": False, "kind": "unreachable",
"error": f"Couldn't reach UniProt for {acc}.",
"next": "Retry; this is usually momentary."}
feats = []
for f in (data.get("features") or []):
sp = _span(f)
if not sp:
continue
ftype = str(f.get("type") or "")
tier = "critical" if ftype in CRITICAL else (
"context" if ftype in CONTEXT else None)
if tier is None:
continue
feats.append({
"type": ftype,
"start": sp[0], "end": sp[1],
"tier": tier,
"description": str(f.get("description") or "")[:180],
"means": CRITICAL.get(ftype) or CONTEXT.get(ftype, ""),
# UniProt's own evidence codes travel with the claim. ECO:0000269
# is experimental; ECO:0000250 is inferred by similarity, and a
# designer should weight those differently.
"evidence": sorted({str(e.get("evidenceCode") or "")
for e in (f.get("evidences") or [])
if e.get("evidenceCode")}),
})
name = ""
try:
name = (data["proteinDescription"]["recommendedName"]
["fullName"]["value"])
except (KeyError, TypeError):
pass
return {
"ok": True,
"accession": data.get("primaryAccession") or acc,
"protein": name,
"length": ((data.get("sequence") or {}).get("length")),
"features": feats,
"critical_count": sum(1 for f in feats if f["tier"] == "critical"),
"source": "UniProt",
}
def annotate(accession: str, positions: Iterable) -> Dict[str, Any]:
"""Which of `positions` land on annotated features.
Positions may be integers or substitution labels ('R175H'); the label form
is what the rest of the engine speaks, so accepting it saves the caller a
parsing step and a chance to get the offset wrong.
"""
table = fetch(accession)
if not table.get("ok"):
return table
wanted: List[Dict[str, Any]] = []
for p in positions or []:
if isinstance(p, int):
wanted.append({"label": str(p), "pos": p})
continue
m = re.match(r"^([A-Za-z])?(\d+)([A-Za-z*])?$", str(p).strip())
if m:
wanted.append({"label": str(p).strip(), "pos": int(m.group(2))})
hits = []
for w in wanted:
on = [f for f in table["features"] if f["start"] <= w["pos"] <= f["end"]]
crit = [f for f in on if f["tier"] == "critical"]
hits.append({
"label": w["label"], "position": w["pos"],
"critical": crit, "context": [f for f in on if f["tier"] == "context"],
# The whole point of the tool, stated so the agent cannot miss it.
"verdict": ("AVOID β annotated functional residue" if crit else
"no functional annotation at this position"),
})
flagged = [h for h in hits if h["critical"]]
return {
"ok": True,
"accession": table["accession"], "protein": table["protein"],
"length": table["length"],
"positions": hits,
"flagged_count": len(flagged),
"verdict": (f"{len(flagged)} of {len(hits)} positions sit on annotated "
f"functional residues." if flagged else
f"None of the {len(hits)} positions are annotated as "
f"functional."),
# The failure that would make this tool actively harmful. UniProt
# numbers the FULL PRECURSOR β bovine trypsin's catalytic triad is at
# 63/107/200 here, not the classic His57/Asp102/Ser195, because the
# signal peptide and propeptide are counted. A user thinking in mature
# or chymotrypsin numbering gets a confident flag on the wrong residue,
# which is worse than no flag at all.
"numbering": (f"Positions are UniProt's, numbered from residue 1 of the "
f"full precursor ({table['length']} aa) including any "
f"signal peptide or propeptide. If you are using mature-"
f"protein or a classic numbering scheme, these will be "
f"offset β check one known residue before trusting the "
f"rest."),
"caveat": ("Absence of annotation is absence of KNOWLEDGE, not evidence "
"that a position is safe β most proteins are annotated "
"sparsely, and an unannotated residue may still be "
"essential. Evidence codes are UniProt's own: ECO:0000269 "
"is experimental, ECO:0000250 is inferred by similarity."),
"source": "UniProt",
}
|