File size: 4,538 Bytes
9591ffa
 
 
 
 
 
9c0338a
9591ffa
9c0338a
 
 
9591ffa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c0338a
 
 
 
 
 
 
9591ffa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c0338a
 
 
 
 
 
9591ffa
 
 
 
 
 
9c0338a
9591ffa
9c0338a
9591ffa
 
 
 
 
 
 
 
 
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
"""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 (MF / BP / CC)
- Per-residue importance scores (saliency map)
- Per-residue amino acid composition

Note: EC number prediction is not implemented in the heuristic model.
"""

from __future__ import annotations

import json
import logging
import urllib.request

logger = logging.getLogger(__name__)


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

    # Fallback: fetch FASTA
    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

    # Predict GO terms based on amino acid composition patterns
    predicted_go = []
    confidence_base = 0.5

    # Simple composition-based predictions
    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),
        })
    if hydrophobic_fraction > 0.4 and charged_fraction > 0.15:
        predicted_go.append({
            "go_id": "GO:0007165",
            "name": "signal transduction",
            "namespace": "BP",
            "confidence": round(min(0.5 + hydrophobic_fraction * 0.2, 0.85), 3),
        })

    # Always include a general prediction
    predicted_go.append({
        "go_id": "GO:0003674",
        "name": "molecular_function",
        "namespace": "MF",
        "confidence": 0.99,
    })

    # Per-residue importance (saliency approximation)
    # Higher importance at charged/polar residues on the surface
    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))

    # Actual per-residue composition (fractions), sorted most abundant first
    composition = {
        "aa": "ACDEFGHIKLMNPQRSTVWY",
        "fractions": {aa: round(aa_comp.get(aa, 0) / max(seq_len, 1), 4) for aa in "ACDEFGHIKLMNPQRSTVWY"},
    }

    return {
        "pdb_id": pdb_id.upper(),
        "sequence_length": seq_len,
        "go_terms": predicted_go,
        "ec_numbers": [],
        "saliency": saliency,
        "composition": composition,
        "method": "heuristic_composition",
        "note": "Predictions based on amino acid composition. EC number prediction is not implemented in this heuristic model; 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)