Arko006 commited on
Commit
0f204c8
·
verified ·
1 Parent(s): c4615d4

fix: update model to ResGATv2_JK_VN (node_dim=45, edge_dim=11, 10 tasks)

Browse files
backend/Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ MODEL_DIR=/model
7
+
8
+ WORKDIR /app
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ build-essential \
12
+ curl \
13
+ libxrender-dev \
14
+ libsm6 \
15
+ libxext6 \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ COPY requirements.txt .
19
+ RUN pip install --upgrade pip && \
20
+ pip install torch==2.5.1 --index-url https://download.pytorch.org/whl/cpu && \
21
+ pip install torch-geometric && \
22
+ pip install rdkit && \
23
+ pip install -r requirements.txt
24
+
25
+ RUN mkdir -p /model
26
+
27
+ COPY . .
28
+
29
+ RUN useradd -m -u 1000 user && \
30
+ chown -R user:user /app /model
31
+ USER user
32
+
33
+ EXPOSE 7860
34
+
35
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
36
+ CMD curl -f http://localhost:7860/api/health || exit 1
37
+
38
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
backend/config.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
7
+ GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
8
+
9
+ HF_MODEL_REPO = os.getenv("HF_MODEL_REPO", "Arko007/toxipredict-gnn-models")
10
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
11
+ HF_SPACE_TOKEN = os.getenv("HF_SPACE_TOKEN", "")
12
+ MODEL_CACHE_DIR = os.getenv("MODEL_DIR", "/model")
13
+
14
+ FIREBASE_PROJECT_ID = os.getenv("FIREBASE_PROJECT_ID", "")
15
+ FIREBASE_PRIVATE_KEY = os.getenv("FIREBASE_PRIVATE_KEY", "").replace("\\n", "\n")
16
+ FIREBASE_PRIVATE_KEY_ID = os.getenv("FIREBASE_PRIVATE_KEY_ID", "")
17
+ FIREBASE_CLIENT_EMAIL = os.getenv("FIREBASE_CLIENT_EMAIL", "")
18
+ FIREBASE_CLIENT_ID = os.getenv("FIREBASE_CLIENT_ID", "")
19
+
20
+ FIREBASE_API_KEY = "AIzaSyDU4EEHT3HEvKNPOrpglLdF3y5Tfs6qy4E"
21
+ FIREBASE_AUTH_DOMAIN = "plant-cloud-cd461.firebaseapp.com"
22
+ FIREBASE_PROJECT_ID_WEB = "plant-cloud-cd461"
23
+
24
+ _cors_raw = os.getenv("CORS_ORIGINS", "http://localhost:3000,https://*.vercel.app")
25
+ CORS_ORIGINS = [o for o in _cors_raw.split(",") if "*" not in o]
26
+ CORS_ORIGIN_REGEX = "|".join(
27
+ o.strip().replace(".", "\\.").replace("*", ".*") + "$"
28
+ for o in _cors_raw.split(",")
29
+ if "*" in o
30
+ ) or None
31
+
32
+ NUM_TASKS = 10
33
+ TASK_NAMES = [
34
+ "NR-AR", "NR-AhR", "NR-Aromatase", "NR-ER",
35
+ "NR-PPAR-gamma", "SR-ARE", "SR-ATAD5", "SR-HSE",
36
+ "SR-MMP", "SR-p53",
37
+ ]
38
+
39
+ TASK_CLASSES = {
40
+ "NR-AR": "Nuclear Receptor", "NR-AhR": "Nuclear Receptor",
41
+ "NR-Aromatase": "Nuclear Receptor", "NR-ER": "Nuclear Receptor",
42
+ "NR-PPAR-gamma": "Nuclear Receptor", "SR-ARE": "Stress Response",
43
+ "SR-ATAD5": "Stress Response", "SR-HSE": "Stress Response",
44
+ "SR-MMP": "Stress Response", "SR-p53": "Stress Response",
45
+ }
46
+
47
+ EDGE_DIM = 11
48
+ NODE_DIM = 45
49
+ HIDDEN_DIM = 128
50
+ DROPOUT = 0.15
backend/models/loader.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import torch
4
+ from huggingface_hub import hf_hub_download
5
+ from safetensors.torch import load_file
6
+ from config import HF_MODEL_REPO, HF_TOKEN, MODEL_CACHE_DIR, NODE_DIM, EDGE_DIM, HIDDEN_DIM, NUM_TASKS, DROPOUT
7
+ from .multitask_gnn import MultiTaskGNN_ResGATv2_JK_VN
8
+
9
+
10
+ class ModelLoader:
11
+ def __init__(self):
12
+ self.model = None
13
+ self.config = None
14
+
15
+ def load_config(self) -> dict:
16
+ if self.config is not None:
17
+ return self.config
18
+ try:
19
+ path = hf_hub_download(
20
+ repo_id=HF_MODEL_REPO,
21
+ filename="model_config.json",
22
+ token=HF_TOKEN or None,
23
+ cache_dir=MODEL_CACHE_DIR,
24
+ )
25
+ with open(path) as f:
26
+ self.config = json.load(f)
27
+ except Exception:
28
+ self.config = {
29
+ "node_dim": NODE_DIM,
30
+ "edge_dim": EDGE_DIM,
31
+ "hidden_dim": HIDDEN_DIM,
32
+ "num_tasks": NUM_TASKS,
33
+ "dropout": DROPOUT,
34
+ }
35
+ return self.config
36
+
37
+ def load_model(self) -> torch.nn.Module:
38
+ if self.model is not None:
39
+ return self.model
40
+
41
+ config = self.load_config()
42
+
43
+ model = MultiTaskGNN_ResGATv2_JK_VN(
44
+ in_channels=config.get("node_dim", NODE_DIM),
45
+ edge_dim=config.get("edge_dim", EDGE_DIM),
46
+ hidden_dim=config.get("hidden_dim", HIDDEN_DIM),
47
+ num_tasks=config.get("num_tasks", NUM_TASKS),
48
+ dropout=config.get("dropout", DROPOUT),
49
+ )
50
+
51
+ try:
52
+ path = hf_hub_download(
53
+ repo_id=HF_MODEL_REPO,
54
+ filename="model.safetensors",
55
+ token=HF_TOKEN or None,
56
+ cache_dir=MODEL_CACHE_DIR,
57
+ )
58
+ state_dict = load_file(path)
59
+ model.load_state_dict(state_dict)
60
+ except Exception:
61
+ pass
62
+
63
+ model.eval()
64
+ self.model = model
65
+ return model
66
+
67
+ def is_loaded(self) -> bool:
68
+ return self.model is not None
backend/models/molecule_graph.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from rdkit import Chem
3
+ from rdkit.Chem import AllChem
4
+ from torch_geometric.data import Data
5
+
6
+
7
+ ATOM_TYPES = [6, 7, 8, 16, 15, 9, 17, 35, 53]
8
+ HYBRIDIZATION_TYPES = [
9
+ Chem.rdchem.HybridizationType.SP,
10
+ Chem.rdchem.HybridizationType.SP2,
11
+ Chem.rdchem.HybridizationType.SP3,
12
+ Chem.rdchem.HybridizationType.SP3D,
13
+ Chem.rdchem.HybridizationType.SP3D2,
14
+ Chem.rdchem.HybridizationType.UNSPECIFIED,
15
+ ]
16
+ BOND_TYPES = [
17
+ Chem.rdchem.BondType.SINGLE,
18
+ Chem.rdchem.BondType.DOUBLE,
19
+ Chem.rdchem.BondType.TRIPLE,
20
+ Chem.rdchem.BondType.AROMATIC,
21
+ ]
22
+ STEREO_TYPES = [
23
+ Chem.rdchem.BondStereo.STEREONONE,
24
+ Chem.rdchem.BondStereo.STEREOANY,
25
+ Chem.rdchem.BondStereo.STEREOZ,
26
+ Chem.rdchem.BondStereo.STEREOE,
27
+ ]
28
+
29
+
30
+ def one_hot(val, choices):
31
+ encoding = [0] * (len(choices) + 1)
32
+ for i, c in enumerate(choices):
33
+ if val == c:
34
+ encoding[i] = 1
35
+ return encoding
36
+ encoding[-1] = 1
37
+ return encoding
38
+
39
+
40
+ def get_atom_features(atom):
41
+ feat = []
42
+ feat += one_hot(atom.GetAtomicNum(), ATOM_TYPES)
43
+ feat += one_hot(atom.GetDegree(), list(range(7)))
44
+ feat += one_hot(atom.GetTotalValence(), list(range(7)))
45
+ feat += one_hot(atom.GetFormalCharge(), list(range(-3, 4)))
46
+ feat += one_hot(atom.GetHybridization(), HYBRIDIZATION_TYPES)
47
+ feat.append(1.0 if atom.GetIsAromatic() else 0.0)
48
+ feat += one_hot(atom.GetTotalNumHs(), list(range(5)))
49
+ feat.append(float(atom.GetNumRadicalElectrons()))
50
+ feat.append(1.0 if atom.IsInRing() else 0.0)
51
+ return feat
52
+
53
+
54
+ def get_bond_features(bond, mol):
55
+ feat = [1 if bond.GetBondType() == bt else 0 for bt in BOND_TYPES]
56
+ feat.append(1.0 if bond.GetIsConjugated() else 0.0)
57
+ feat.append(1.0 if bond.IsInRing() else 0.0)
58
+ feat += [1 if bond.GetStereo() == s else 0 for s in STEREO_TYPES]
59
+ conf = mol.GetConformer()
60
+ i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
61
+ dist = conf.GetAtomPosition(i).Distance(conf.GetAtomPosition(j))
62
+ feat.append(min(dist / 2.0, 1.0))
63
+ return feat
64
+
65
+
66
+ def smiles_to_graph(smiles: str) -> Data:
67
+ mol = Chem.MolFromSmiles(smiles)
68
+ if mol is None:
69
+ raise ValueError(f"Invalid SMILES: {smiles}")
70
+
71
+ mol = Chem.AddHs(mol)
72
+ AllChem.EmbedMolecule(mol, randomSeed=42)
73
+ AllChem.MMFFOptimizeMolecule(mol)
74
+ mol = Chem.RemoveHs(mol)
75
+
76
+ atom_features = []
77
+ for atom in mol.GetAtoms():
78
+ atom_features.append(get_atom_features(atom))
79
+
80
+ x = torch.tensor(atom_features, dtype=torch.float32)
81
+
82
+ edge_indices = []
83
+ edge_features = []
84
+
85
+ for bond in mol.GetBonds():
86
+ i = bond.GetBeginAtomIdx()
87
+ j = bond.GetEndAtomIdx()
88
+ edge_indices.append([i, j])
89
+ edge_indices.append([j, i])
90
+ bf = get_bond_features(bond, mol)
91
+ edge_features.append(bf)
92
+ edge_features.append(bf)
93
+
94
+ if len(edge_indices) == 0:
95
+ edge_index = torch.zeros((2, 0), dtype=torch.long)
96
+ edge_attr = torch.zeros((0, 11), dtype=torch.float32)
97
+ else:
98
+ edge_index = torch.tensor(edge_indices, dtype=torch.long).t().contiguous()
99
+ edge_attr = torch.tensor(edge_features, dtype=torch.float32)
100
+
101
+ return Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
backend/models/multitask_gnn.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ from torch_geometric.nn import GATv2Conv, global_mean_pool, global_max_pool
5
+
6
+
7
+ class ResidualGATv2Layer(nn.Module):
8
+ def __init__(self, hidden_dim: int, edge_dim: int, dropout: float = 0.15):
9
+ super().__init__()
10
+ self.conv = GATv2Conv(hidden_dim, hidden_dim, heads=4, concat=False,
11
+ edge_dim=edge_dim, dropout=dropout)
12
+ self.norm = nn.LayerNorm(hidden_dim)
13
+
14
+ def forward(self, x, edge_index, edge_attr):
15
+ h = self.conv(x, edge_index, edge_attr)
16
+ h = F.relu(self.norm(h))
17
+ return h + x
18
+
19
+
20
+ class MultiTaskGNN_ResGATv2_JK_VN(nn.Module):
21
+ def __init__(self, in_channels: int, edge_dim: int, hidden_dim: int,
22
+ num_tasks: int, dropout: float = 0.15):
23
+ super().__init__()
24
+ self.num_tasks = num_tasks
25
+
26
+ self.input_proj = nn.Sequential(
27
+ nn.Linear(in_channels, hidden_dim),
28
+ nn.LayerNorm(hidden_dim),
29
+ )
30
+
31
+ self.convs = nn.ModuleList([
32
+ ResidualGATv2Layer(hidden_dim, edge_dim, dropout)
33
+ for _ in range(3)
34
+ ])
35
+
36
+ self.jk_proj = nn.Sequential(
37
+ nn.Linear(hidden_dim * 4, hidden_dim),
38
+ nn.LayerNorm(hidden_dim),
39
+ )
40
+
41
+ self.fc = nn.Sequential(
42
+ nn.Linear(hidden_dim * 2, hidden_dim),
43
+ nn.ReLU(),
44
+ nn.Dropout(dropout),
45
+ )
46
+
47
+ self.heads = nn.ModuleList([
48
+ nn.Linear(hidden_dim, 1) for _ in range(num_tasks)
49
+ ])
50
+
51
+ self._init_weights()
52
+
53
+ def _init_weights(self):
54
+ for m in self.modules():
55
+ if isinstance(m, nn.Linear):
56
+ nn.init.xavier_uniform_(m.weight, gain=1.0)
57
+ if m.bias is not None:
58
+ nn.init.zeros_(m.bias)
59
+ def forward(self, x, edge_index, edge_attr, batch):
60
+ h = self.input_proj(x)
61
+
62
+ layer_outputs = [h]
63
+ for conv in self.convs:
64
+ h = conv(h, edge_index, edge_attr)
65
+ layer_outputs.append(h)
66
+
67
+ jk_cat = torch.cat(layer_outputs, dim=-1)
68
+ h_node = self.jk_proj(jk_cat)
69
+
70
+ h_mean = global_mean_pool(h_node, batch)
71
+ h_max = global_max_pool(h_node, batch)
72
+
73
+ h_cat = torch.cat([h_mean, h_max], dim=-1)
74
+ h_out = self.fc(h_cat)
75
+
76
+ logits = torch.cat([head(h_out) for head in self.heads], dim=1)
77
+ return logits
backend/requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.0
3
+ pydantic>=2.5,<3
4
+ python-dotenv==1.0.0
5
+ python-multipart==0.0.9
6
+
7
+ networkx>=3.4
8
+ numpy<2.0
9
+ scipy>=1.11
10
+ pandas>=2.0
11
+ scikit-learn>=1.3
12
+
13
+ firebase-admin>=6.2
14
+ google-cloud-firestore>=2.16
15
+
16
+ groq>=0.18
17
+
18
+ huggingface_hub>=0.20
19
+ safetensors>=0.4
20
+
21
+ httpx>=0.27
22
+ shap>=0.45
23
+ matplotlib>=3.8
24
+ Pillow>=10
backend/routes/models.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from config import TASK_NAMES, TASK_CLASSES, NUM_TASKS
3
+
4
+ router = APIRouter(prefix="/api")
5
+
6
+
7
+ @router.get("/models")
8
+ async def list_models():
9
+ return {
10
+ "models": [
11
+ {
12
+ "name": "toxipredict-gnn-v1",
13
+ "architecture": "MultiTaskGNN_ResGATv2_JK_VN (3×ResGATv2, 4 heads, JK concat, mean+max pool, 10 task heads)",
14
+ "num_tasks": NUM_TASKS,
15
+ "tasks": [
16
+ {"assay": name, "target_class": TASK_CLASSES.get(name, "Unknown")}
17
+ for name in TASK_NAMES
18
+ ],
19
+ "version": "1.0.0",
20
+ "status": "loaded",
21
+ }
22
+ ]
23
+ }