Samad14's picture
feat: optimize CASTp SASA with KDTree, add sequence input to CASTp + Structure Prep, ESMFold integration for unknown genes
e21c532
Raw History Blame Contribute Delete
5.93 kB
"""CASTp pocket/cavity analysis via Biopython SASA + local detection."""
import asyncio
import logging
import math
from typing import Any
import httpx
logger = logging.getLogger(__name__)
CASTPFOLD_BASE = "https://cfold.bme.uic.edu/castpfold"
async def analyze_pockets_pdb_id(pdb_id: str, probe_radius: float = 1.4) -> dict:
pdb_text = await _fetch_pdb(pdb_id)
return await _analyze_pockets(pdb_text, pdb_id, probe_radius)
async def analyze_pockets_pdb_text(pdb_text: str, pdb_id: str = "custom", probe_radius: float = 1.4) -> dict:
return await _analyze_pockets(pdb_text, pdb_id, probe_radius)
async def _fetch_pdb(pdb_id: str) -> str:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.get(f"https://files.rcsb.org/download/{pdb_id.upper()}.pdb")
resp.raise_for_status()
return resp.text
def _analyze_pockets_sync(pdb_text: str, pdb_id: str, probe_radius: float) -> dict:
"""Compute per-residue SASA and detect pockets via clustering (CPU-bound)."""
import io
from Bio.PDB import PDBParser, SASA
parser = PDBParser(QUIET=True)
structure = parser.get_structure(pdb_id, io.StringIO(pdb_text))
sr = SASA.ShrakeRupley()
sr.compute(structure[0], level="R")
residues_sasa: list[dict] = []
coords: list[tuple[float, float, float]] = []
for chain in structure[0]:
for residue in chain:
if residue.id[0] != " ":
continue
sasa_val = residue.sasa
ca = None
for atom in residue:
if atom.name == "CA":
ca = atom.coord
break
if ca is None:
continue
residues_sasa.append({
"chain": chain.id,
"residue": residue.resname,
"resnum": residue.id[1],
"sasa": round(float(sasa_val), 2),
"coords": [round(float(c), 3) for c in ca],
})
coords.append((float(ca[0]), float(ca[1]), float(ca[2])))
pockets = _detect_pockets_fast(residues_sasa, coords, probe_radius)
return {
"pdb_id": pdb_id,
"probe_radius": probe_radius,
"total_residues": len(residues_sasa),
"pockets": pockets,
"residues": residues_sasa,
}
async def _analyze_pockets(pdb_text: str, pdb_id: str, probe_radius: float) -> dict:
import functools
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None, functools.partial(_analyze_pockets_sync, pdb_text, pdb_id, probe_radius)
)
def _detect_pockets_fast(residues: list[dict], coords: list[tuple], probe_radius: float) -> list[dict]:
"""Detect pockets using scipy KDTree for O(n log n) neighbor lookups."""
if not residues:
return []
exposed = [(i, r) for i, r in enumerate(residues) if r["sasa"] > 1.0]
if len(exposed) < 5:
return []
exposed_coords = [coords[i] for i, _ in exposed]
n = len(exposed_coords)
try:
from scipy.spatial import KDTree
cutoff = 8.0 + probe_radius * 2
tree = KDTree(exposed_coords)
pairs = tree.query_pairs(r=cutoff, output_type='ndarray')
adj: dict[int, list[int]] = {i: [] for i in range(n)}
orig_idx = {i: exposed[i][0] for i in range(n)}
for a, b in pairs:
adj[a].append(b)
adj[b].append(a)
except ImportError:
adj = _build_adj_brute(exposed_coords, probe_radius)
visited = set()
raw_pockets = []
for start in range(n):
if start in visited:
continue
queue = [start]
cluster = []
while queue:
node = queue.pop()
if node in visited:
continue
visited.add(node)
cluster.append(node)
for nb in adj[node]:
if nb not in visited:
queue.append(nb)
if len(cluster) >= 5:
raw_pockets.append(cluster)
raw_pockets.sort(key=lambda c: -len(c))
pockets = []
for idx, cluster_indices in enumerate(raw_pockets):
cluster_residues = [exposed[i][1] for i in cluster_indices]
centroid = [0.0, 0.0, 0.0]
for r in cluster_residues:
for k in range(3):
centroid[k] += r["coords"][k]
for k in range(3):
centroid[k] /= len(cluster_residues)
max_dist = 0.0
for r in cluster_residues:
dx = r["coords"][0] - centroid[0]
dy = r["coords"][1] - centroid[1]
dz = r["coords"][2] - centroid[2]
d = math.sqrt(dx * dx + dy * dy + dz * dz)
if d > max_dist:
max_dist = d
volume = (4.0 / 3.0) * math.pi * (max_dist + probe_radius) ** 3
avg_sasa = sum(r["sasa"] for r in cluster_residues) / len(cluster_residues)
pockets.append({
"id": idx + 1,
"area_sa": round(avg_sasa * len(cluster_residues), 1),
"volume_sa": round(volume, 1),
"num_residues": len(cluster_residues),
"residues": [
f"{r['chain']}{r['resnum']}{r['residue']}" for r in cluster_residues
],
"centroid": [round(c, 2) for c in centroid],
"radius": round(max_dist + probe_radius, 2),
})
return pockets
def _build_adj_brute(coords: list[tuple], probe_radius: float) -> dict[int, list[int]]:
n = len(coords)
adj: dict[int, list[int]] = {i: [] for i in range(n)}
cutoff = 8.0 + probe_radius * 2
for i in range(n):
for j in range(i + 1, n):
dx = coords[i][0] - coords[j][0]
dy = coords[i][1] - coords[j][1]
dz = coords[i][2] - coords[j][2]
if dx * dx + dy * dy + dz * dz < cutoff * cutoff:
adj[i].append(j)
adj[j].append(i)
return adj