File size: 5,783 Bytes
5d4afe2
 
 
d686612
 
 
 
 
 
5d4afe2
d686612
 
 
4bb4db8
 
d686612
 
 
 
 
 
 
 
 
 
 
 
 
4bb4db8
012754b
4bb4db8
 
012754b
d686612
 
 
 
 
 
 
 
 
 
 
 
012754b
5d4afe2
d686612
 
 
5d4afe2
d686612
 
 
 
 
 
 
 
 
 
 
 
4bb4db8
 
d686612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4bb4db8
d686612
 
 
 
 
 
 
 
 
 
 
5d4afe2
d686612
 
 
012754b
d686612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4afe2
d686612
 
 
 
5d4afe2
d686612
 
 
 
 
 
 
5d4afe2
d686612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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"]
    }