| """Protein function prediction using a simplified GCN-like approach. |
| |
| This is a lightweight approximation inspired by DeepFRI. For production use, |
| bake the full DeepFRI weights into the Docker image (see Dockerfile additions). |
| |
| Outputs: |
| - GO term predictions with confidence scores |
| - EC number predictions with confidence scores |
| - Per-residue importance scores (saliency map) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import logging |
| import urllib.request |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| _GO_MAPPINGS = { |
| "hydrolase": ("GO:0003824", "hydrolase activity", "MF"), |
| "transferase": ("GO:0016740", "transferase activity", "MF"), |
| "oxidoreductase": ("GO:0016491", "oxidoreductase activity", "MF"), |
| "lyase": ("GO:0016829", "lyase activity", "MF"), |
| "isomerase": ("GO:0016853", "isomerase activity", "MF"), |
| "ligase": ("GO:0016874", "ligase activity", "MF"), |
| "kinase": ("GO:0016301", "kinase activity", "MF"), |
| "protease": ("GO:0008233", "peptidase activity", "MF"), |
| "receptor": ("GO:0004872", "receptor activity", "MF"), |
| "binding": ("GO:0005488", "binding", "MF"), |
| "transporter": ("GO:0005215", "transporter activity", "MF"), |
| "signal": ("GO:0005515", "protein binding", "MF"), |
| "cytoplasm": ("GO:0005737", "cytoplasm", "CC"), |
| "nucleus": ("GO:0005634", "nucleus", "CC"), |
| "membrane": ("GO:0016020", "membrane", "CC"), |
| "mitochondrion": ("GO:0005739", "mitochondrion", "CC"), |
| "cell": ("GO:0005623", "cell", "CC"), |
| "response": ("GO:0050789", "regulation of biological process", "BP"), |
| "phosphorylation": ("GO:0016310", "phosphorylation", "BP"), |
| "transcription": ("GO:0006351", "transcription, DNA-templated", "BP"), |
| "translation": ("GO:0006412", "translation", "BP"), |
| "apoptosis": ("GO:0006915", "apoptotic process", "BP"), |
| "cell_cycle": ("GO:0007049", "cell cycle", "BP"), |
| "immune": ("GO:0006955", "immune response", "BP"), |
| } |
|
|
|
|
| def _fetch_pdb_sequence(pdb_id: str) -> str: |
| """Fetch the amino acid sequence for a PDB entry from RCSB.""" |
| url = f"https://data.rcsb.org/rest/v1/core/polymer_entity/{pdb_id}/1" |
| try: |
| data = json.loads(urllib.request.urlopen(url, timeout=15).read()) |
| return data.get("entity_poly", {}).get("pdbx_seq_one_letter_code_can", "") |
| except Exception: |
| pass |
|
|
| |
| try: |
| url = f"https://www.rcsb.org/fasta/entry/{pdb_id}" |
| text = urllib.request.urlopen(url, timeout=15).read().decode() |
| lines = [l for l in text.splitlines() if not l.startswith(">")] |
| return "".join(lines).replace("\n", "") |
| except Exception as e: |
| raise RuntimeError(f"Could not fetch sequence for {pdb_id}: {e}") |
|
|
|
|
| def _predict_from_sequence(sequence: str, pdb_id: str) -> dict: |
| """Lightweight function prediction based on sequence composition. |
| |
| This is a heuristic approximation. Replace with proper GCN inference |
| when DeepFRI weights are baked into the Docker image. |
| """ |
| seq_upper = sequence.upper() |
| seq_len = len(seq_upper) |
| aa_comp = {} |
| for aa in seq_upper: |
| aa_comp[aa] = aa_comp.get(aa, 0) + 1 |
|
|
| |
| predicted_go = [] |
| confidence_base = 0.5 |
|
|
| |
| hydrophobic_fraction = sum(aa_comp.get(a, 0) for a in "AILMFWV") / max(seq_len, 1) |
| charged_fraction = sum(aa_comp.get(a, 0) for a in "DEKRH") / max(seq_len, 1) |
|
|
| if hydrophobic_fraction > 0.4: |
| predicted_go.append({ |
| "go_id": "GO:0016020", |
| "name": "membrane", |
| "namespace": "CC", |
| "confidence": round(min(0.6 + hydrophobic_fraction * 0.3, 0.95), 3), |
| }) |
| if charged_fraction > 0.25: |
| predicted_go.append({ |
| "go_id": "GO:0005515", |
| "name": "protein binding", |
| "namespace": "MF", |
| "confidence": round(min(0.55 + charged_fraction * 0.2, 0.9), 3), |
| }) |
|
|
| |
| predicted_go.append({ |
| "go_id": "GO:0003674", |
| "name": "molecular_function", |
| "namespace": "MF", |
| "confidence": 0.99, |
| }) |
|
|
| |
| |
| saliency = [] |
| for i, aa in enumerate(seq_upper): |
| score = 0.1 |
| if aa in "DEKRH": |
| score = 0.6 |
| elif aa in "STNQ": |
| score = 0.4 |
| elif aa in "AGV": |
| score = 0.2 |
| else: |
| score = 0.15 |
| saliency.append(round(score, 3)) |
|
|
| return { |
| "pdb_id": pdb_id.upper(), |
| "sequence_length": seq_len, |
| "go_terms": predicted_go, |
| "ec_numbers": [], |
| "saliency": saliency, |
| "method": "heuristic_composition", |
| "note": "Predictions based on amino acid composition. For research-grade predictions, use the full DeepFRI model.", |
| } |
|
|
|
|
| def predict_function(pdb_id: str) -> dict: |
| """Main entry point: predict protein function from structure.""" |
| sequence = _fetch_pdb_sequence(pdb_id) |
| if not sequence: |
| raise RuntimeError(f"No sequence available for PDB {pdb_id}") |
| return _predict_from_sequence(sequence, pdb_id) |
|
|