File size: 4,459 Bytes
2fd4f2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
High-Throughput Virtual Screening & Batch Profiling Engine.
Screens libraries of candidate SMILES across ESOL, BBBP, Tox21, and ADMET filters,
and ranks compounds using a Multi-Objective Drug Desirability Score.
"""
from typing import List, Dict, Any, Optional
import pandas as pd
import numpy as np
from rdkit import Chem, RDLogger
from rdkit.Chem import Descriptors, QED

from src.admet import compute_admet_profile
from src.ensemble import predict_all_endpoints

RDLogger.DisableLog('rdApp.*')


def calculate_desirability_score(
    qed: float,
    lipinski_pass: bool,
    log_s: float,
    max_tox_prob: float,
    has_pains: bool,
    prefer_bbb: bool = False,
    bbb_prob: float = 0.5
) -> float:
    """
    Compute a composite Drug Desirability Score between 0.0 and 100.0.
    """
    # 1. QED component (0 - 30 pts)
    score_qed = float(np.clip(qed, 0.0, 1.0)) * 30.0

    # 2. Lipinski compliance (0 or 20 pts)
    score_lipinski = 20.0 if lipinski_pass else 0.0

    # 3. Solubility (0 - 20 pts) - optimal between -1 and -3
    if log_s >= -2.0:
        score_sol = 20.0
    elif log_s >= -4.0:
        score_sol = 15.0
    elif log_s >= -6.0:
        score_sol = 8.0
    else:
        score_sol = 2.0

    # 4. Toxicity Safety (0 - 20 pts)
    score_tox = max(0.0, (1.0 - max_tox_prob) * 20.0)

    # 5. Assay Interference penalty (-10 pts if PAINS alert)
    penalty_pains = -10.0 if has_pains else 0.0

    # 6. BBB adjustment (0 - 10 pts)
    if prefer_bbb:
        score_bbb = bbb_prob * 10.0
    else:
        # Non-CNS drug preference (lower BBB is better)
        score_bbb = (1.0 - bbb_prob) * 10.0

    total = max(0.0, min(100.0, score_qed + score_lipinski + score_sol + score_tox + score_bbb + penalty_pains))
    return round(total, 1)


def screen_batch_molecules(
    smiles_list: List[str],
    compound_names: Optional[List[str]] = None,
    prefer_bbb: bool = False,
    progress_callback: Optional[Any] = None
) -> pd.DataFrame:
    """
    Screen a collection of SMILES strings and return a ranked DataFrame.
    """
    records = []
    total = len(smiles_list)

    for idx, smi in enumerate(smiles_list):
        smi = str(smi).strip()
        if not smi:
            continue

        name = compound_names[idx] if compound_names and idx < len(compound_names) else f"Compound_{idx+1}"
        mol = Chem.MolFromSmiles(smi)
        if mol is None:
            continue

        try:
            admet = compute_admet_profile(mol)
            preds = predict_all_endpoints(smi)

            esol_val = float(preds['esol']['mean'][0])
            bbbp_prob = float(preds['bbbp']['mean'][0])
            tox_probs = preds['tox21']['mean']
            max_tox = float(np.max(tox_probs))
            mean_tox = float(np.mean(tox_probs))
            high_risk_tox_count = int(np.sum(tox_probs >= 0.5))

            qed_score = admet['descriptors']['QED']
            lip_pass = admet['filters']['Lipinski']['passed']
            pains_alert = admet['alerts']['PAINS']['has_alerts']

            desirability = calculate_desirability_score(
                qed=qed_score,
                lipinski_pass=lip_pass,
                log_s=esol_val,
                max_tox_prob=max_tox,
                has_pains=pains_alert,
                prefer_bbb=prefer_bbb,
                bbb_prob=bbbp_prob
            )

            records.append({
                "Compound ID": name,
                "SMILES": smi,
                "Desirability Score": desirability,
                "QED": round(qed_score, 3),
                "Solubility logS": round(esol_val, 2),
                "BBB Probability": f"{bbbp_prob*100:.1f}%",
                "Max Tox Risk": f"{max_tox*100:.1f}%",
                "High Risk Assays": high_risk_tox_count,
                "MW (Da)": round(admet['descriptors']['Molecular Weight'], 1),
                "LogP": round(admet['descriptors']['LogP'], 2),
                "TPSA (Ų)": round(admet['descriptors']['TPSA'], 1),
                "Lipinski": "PASS" if lip_pass else f"FAIL ({admet['filters']['Lipinski']['num_violations']})",
                "PAINS Alert": "YES" if pains_alert else "Clean"
            })
        except Exception:
            continue

        if progress_callback:
            progress_callback((idx + 1) / total)

    df = pd.DataFrame(records)
    if not df.empty:
        df = df.sort_values(by="Desirability Score", ascending=False).reset_index(drop=True)
    return df