syntheogenesis / dee /core /features.py
github-actions[bot]
Deploy 90a38dc
1bf37a2
Raw
History Blame Contribute Delete
8.58 kB
"""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",
}