Spaces:
Sleeping
Sleeping
fix: update model to ResGATv2_JK_VN (node_dim=45, edge_dim=11, 10 tasks)
Browse files- backend/Dockerfile +38 -0
- backend/config.py +50 -0
- backend/models/loader.py +68 -0
- backend/models/molecule_graph.py +101 -0
- backend/models/multitask_gnn.py +77 -0
- backend/requirements.txt +24 -0
- backend/routes/models.py +23 -0
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 |
+
}
|