EpiADR-Net / api.py
ADjayantan
Fix GitHub Actions CI: Resolved ruff linter check and updated CI workflow flags
5d4afe2
Raw
History Blame Contribute Delete
5.78 kB
import os
import torch
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from data_loader import GTEX_TISSUE_PROFILES, MEDDRA_ADR_CLASSES, ORGAN_NAMES
from model import EpiADRNet
from utils import highlight_toxic_subgraph, smiles_to_graph
app = FastAPI(
title="EpiADR-Net REST Microservice API",
description="Enterprise API for Tissue-Conditioned Zero-Shot Adverse Drug Reaction (ADR) Disaggregation — v5 Foundation Edition (100M+ Parameters)",
version="5.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# Enable CORS for frontend integration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global Model Initialization — EpiADR-Net v5 (~116.5M Parameters)
MODEL = EpiADRNet(
in_features=24, hidden_dim=1536, tissue_dim=1024,
num_classes=10, num_gat_layers=12, num_heads=16, dropout=0.1,
)
MODEL_WEIGHTS_PATH = "model.pt"
if os.path.exists(MODEL_WEIGHTS_PATH):
try:
MODEL.load_state_dict(torch.load(MODEL_WEIGHTS_PATH, map_location=torch.device('cpu')))
MODEL.eval()
except Exception:
pass
# Pydantic Schemas
class PredictionRequest(BaseModel):
smiles: str = Field(..., json_schema_extra={"example": "CC(=O)NC1=CC=C(O)C=C1"}, description="Valid molecular SMILES string")
organ: str = Field(..., json_schema_extra={"example": "Liver"}, description="Target human tissue profile (Liver, Heart, Brain, Kidney, Lung, Pancreas, Spleen, Intestine, Skin, Bone_Marrow)")
mc_samples: int | None = Field(20, ge=5, le=50, description="Monte Carlo Dropout sample passes")
class ExplainRequest(BaseModel):
smiles: str = Field(..., json_schema_extra={"example": "O=C1C2=C(O)C=CC=C2C(=O)C3=C1C(O)=C4C(=C3O)C(O)(C(=O)CO)CC(O)C4"})
top_k: int | None = Field(3, ge=1, le=10)
class CompareRequest(BaseModel):
smiles: str = Field(..., json_schema_extra={"example": "CC(=O)NC1=CC=C(O)C=C1"})
organ_a: str = Field(..., json_schema_extra={"example": "Liver"})
organ_b: str = Field(..., json_schema_extra={"example": "Heart"})
@app.get("/", tags=["System Health"])
def root():
return {
"status": "online",
"service": "EpiADR-Net REST Microservice",
"version": "5.0.0",
"architecture": "12-Layer Graph Transformer + SwiGLU FFN + DMPNN | 1024-dim GTEx | 116.5M params",
"documentation": "/docs"
}
@app.get("/health", tags=["System Health"])
def health_check():
return {
"status": "healthy",
"model_loaded": True,
"device": "cpu",
"organs": ORGAN_NAMES,
"num_adr_classes": len(MEDDRA_ADR_CLASSES)
}
@app.get("/organs", tags=["Reference Metadata"])
def get_supported_organs():
return {"supported_organs": ORGAN_NAMES, "vector_dimension": 1024}
@app.get("/adr-classes", tags=["Reference Metadata"])
def get_adr_classes():
return {"meddra_terms": MEDDRA_ADR_CLASSES}
@app.post("/predict", tags=["ADR Inference"])
def predict_adr(req: PredictionRequest):
if req.organ not in GTEX_TISSUE_PROFILES:
raise HTTPException(status_code=400, detail=f"Unsupported organ '{req.organ}'. Choose from {ORGAN_NAMES}")
try:
node_feats, edge_index, _atom_symbols = smiles_to_graph(req.smiles)
batch = torch.zeros(node_feats.size(0), dtype=torch.long)
tissue_vec = GTEX_TISSUE_PROFILES[req.organ].unsqueeze(0)
mc_res = MODEL.predict_mc_dropout(node_feats, edge_index, batch, tissue_vec, num_samples=req.mc_samples or 30)
mu = mc_res["mean_probabilities"][0].cpu().numpy()
sigma = mc_res["uncertainty_sigma"][0].cpu().numpy()
attn = mc_res["attention_weights"]
predictions = []
for i, term in enumerate(MEDDRA_ADR_CLASSES):
predictions.append({
"meddra_term": term,
"predicted_probability": float(round(mu[i], 4)),
"epistemic_uncertainty": float(round(sigma[i], 4)),
"is_significant": bool(mu[i] > 0.45)
})
xai_breakdown = highlight_toxic_subgraph(req.smiles, attn, top_k=3)
return {
"smiles": req.smiles,
"conditioned_organ": req.organ,
"predictions": predictions,
"xai_explanation": xai_breakdown
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Inference failed: {e!s}")
@app.post("/explain", tags=["Explainable AI"])
def explain_subgraph(req: ExplainRequest):
try:
node_feats, edge_index, _atom_symbols = smiles_to_graph(req.smiles)
batch = torch.zeros(node_feats.size(0), dtype=torch.long)
tissue_vec = GTEX_TISSUE_PROFILES["Liver"].unsqueeze(0)
_, attn = MODEL(node_feats, edge_index, batch, tissue_vec, return_attention=True)
explanation = highlight_toxic_subgraph(req.smiles, attn, top_k=req.top_k)
return explanation
except Exception as e:
raise HTTPException(status_code=500, detail=f"XAI extraction failed: {e!s}")
@app.post("/compare-organs", tags=["Comparative Analysis"])
def compare_organs(req: CompareRequest):
if req.organ_a not in GTEX_TISSUE_PROFILES or req.organ_b not in GTEX_TISSUE_PROFILES:
raise HTTPException(status_code=400, detail="Invalid organ selected")
res_a = predict_adr(PredictionRequest(smiles=req.smiles, organ=req.organ_a))
res_b = predict_adr(PredictionRequest(smiles=req.smiles, organ=req.organ_b))
return {
"smiles": req.smiles,
"organ_a": req.organ_a,
"organ_b": req.organ_b,
"predictions_organ_a": res_a["predictions"],
"predictions_organ_b": res_b["predictions"]
}