rascore-api / app.py
ilkhamfy's picture
Add SA score to /score (one endpoint returns SA + RAScore + SCScore)
b591205 verified
Raw
History Blame Contribute Delete
4.21 kB
"""
RAScore micro-service for MolParetoLab (isolated from the ADMET-AI Space).
Retrosynthetic Accessibility score (Thakkar et al., Chem. Sci. 2021,
doi:10.1039/D0SC05401A): the probability that AiZynthFinder can find a synthesis
route to a molecule. 0 = hard / no route found, 1 = readily synthesizable.
Also returns SCScore (Coley 2018 synthetic complexity, 1 easy - 5 hard).
POST /score {"smiles": ["...", ...]} -> {"results": [{"smiles","RAScore","SCScore"}, ...]}
CORS-enabled, no API key. Deploy as an HF Space (Docker SDK) or local Docker.
"""
from fastapi import FastAPI, HTTPException, Response
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List
import json, logging, os
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="RAScore API", description="Retrosynthetic accessibility score for MolParetoLab", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
)
_scorer = None
def get_scorer():
global _scorer
if _scorer is None:
logger.info("Loading RAScore XGB model...")
from RAscore import RAscore_XGB
_scorer = RAscore_XGB.RAScorerXGB()
logger.info("RAScore model loaded.")
return _scorer
def ra_score(smiles: str):
try:
from rdkit import Chem
if Chem.MolFromSmiles(smiles) is None:
return None
return round(float(get_scorer().predict(smiles)), 4)
except Exception:
import traceback
logger.error(f"RAScore failed for {smiles}:\n{traceback.format_exc()}")
return None
# ── SA score: Ertl & Schuffenhauer 2009 (synthetic accessibility, 1 easy – 10 hard),
# via RDKit's Contrib sascorer β€” so this one endpoint returns all three make-ability scores.
_sascorer = None
def sa_score(smiles: str):
global _sascorer
try:
if _sascorer is None:
import sys, os as _os
from rdkit.Chem import RDConfig
sys.path.append(_os.path.join(RDConfig.RDContribDir, "SA_Score"))
import sascorer as _sa
_sascorer = _sa
from rdkit import Chem
m = Chem.MolFromSmiles(smiles)
return round(_sascorer.calculateScore(m), 2) if m is not None else None
except Exception:
import traceback
logger.error(f"SA score failed for {smiles}:\n{traceback.format_exc()}")
return None
# ── SCScore: Coley et al., J. Chem. Inf. Model. 2018 (synthetic complexity, 1 easy – 5 hard).
# Standalone numpy model (no TensorFlow) + 1024-bit folded-FP weights, fetched in the image.
_scscorer = None
def get_scscorer():
global _scscorer
if _scscorer is None:
import sys
if "/app" not in sys.path:
sys.path.append("/app")
logger.info("Loading SCScore model...")
from scscore_standalone import SCScorer
s = SCScorer()
s.restore("/app/scscore_1024bool.json.gz", FP_rad=2, FP_len=1024)
_scscorer = s
logger.info("SCScore model loaded.")
return _scscorer
def sc_score(smiles: str):
try:
_, score = get_scscorer().get_score_from_smi(smiles)
return round(float(score), 3)
except Exception:
import traceback
logger.error(f"SCScore failed for {smiles}:\n{traceback.format_exc()}")
return None
class ScoreRequest(BaseModel):
smiles: List[str]
@app.get("/health")
def health():
return {"status": "ok", "model_loaded": _scorer is not None}
@app.post("/score")
def score(req: ScoreRequest):
if not req.smiles:
raise HTTPException(400, "smiles list is empty")
if len(req.smiles) > 1000:
raise HTTPException(400, "Maximum 1000 SMILES per request")
results = [{"smiles": s, "SA_Score": sa_score(s), "RAScore": ra_score(s), "SCScore": sc_score(s)} for s in req.smiles]
return Response(content=json.dumps({"results": results}), media_type="application/json")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))