File size: 5,715 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
"""
In Silico Lead Optimization & Molecular Analog Generator.
Systematically generates bioisosteric and R-group substituted chemical analogs,
evaluates property shifts (Delta logS, Delta BBBP, Delta Tox), and discovers Pareto-optimal leads.
"""
from typing import List, Dict, Any, Optional
from rdkit import Chem, RDLogger
from rdkit.Chem import AllChem, Descriptors, QED

from src.admet import compute_admet_profile
from src.ensemble import predict_ensemble

RDLogger.DisableLog('rdApp.*')

# Common medicinal chemistry transformation rules
MEDCHEM_RULES = [
    # Aromatic functional group replacements
    {"name": "+Fluorine (-F)", "smarts": "[c:1][H]", "repl": "[c:1]F", "rationale": "Improves metabolic stability & lipophilicity"},
    {"name": "+Chlorine (-Cl)", "smarts": "[c:1][H]", "repl": "[c:1]Cl", "rationale": "Enhances binding cavity packing"},
    {"name": "+Methyl (-CH3)", "smarts": "[c:1][H]", "repl": "[c:1]C", "rationale": "Fills hydrophobic subpocket"},
    {"name": "+Trifluoromethyl (-CF3)", "smarts": "[c:1][H]", "repl": "[c:1]C(F)(F)F", "rationale": "Strongly enhances lipophilic binding"},
    {"name": "+Hydroxyl (-OH)", "smarts": "[c:1][H]", "repl": "[c:1]O", "rationale": "Improves aqueous solubility (H-bond donor)"},
    {"name": "+Methoxy (-OCH3)", "smarts": "[c:1][H]", "repl": "[c:1]OC", "rationale": "H-bond acceptor without acidic proton"},
    {"name": "+Amino (-NH2)", "smarts": "[c:1][H]", "repl": "[c:1]N", "rationale": "Basic center, enhances ionic interactions"},
    {"name": "+Cyano (-C#N)", "smarts": "[c:1][H]", "repl": "[c:1]C#N", "rationale": "Linear polar group, bioisosteric to halide"},
    {"name": "+Methylsulfonyl (-SO2CH3)", "smarts": "[c:1][H]", "repl": "[c:1]S(=O)(=O)C", "rationale": "High polarity, decreases lipophilicity"},
    
    # Functional group bioisosteres
    {"name": "Ester -> Amide", "smarts": "[C:1](=O)[O:2][C:3]", "repl": "[C:1](=O)[N:2][C:3]", "rationale": "Improves hydrolytic stability in plasma"},
    {"name": "Acid -> Tetrazole (Bioisostere)", "smarts": "[C:1](=O)[OH]", "repl": "[C:1]c1nnnn1", "rationale": "Non-classical carboxylic acid bioisostere"},
    {"name": "Phenol -> Fluoro-benzene", "smarts": "[c:1][OH]", "repl": "[c:1]F", "rationale": "Blocks Phase-II glucuronidation metabolism"}
]


def generate_analogs(parent_smiles: str, max_analogs: int = 15) -> List[Dict[str, Any]]:
    """
    Generate chemically plausible analogs of the parent molecule.
    
    Args:
        parent_smiles: SMILES string of the lead compound.
        max_analogs: Maximum number of distinct valid analogs to generate.
        
    Returns:
        List of generated analog dictionaries with SMILES and transformation details.
    """
    mol = Chem.MolFromSmiles(parent_smiles)
    if mol is None:
        return []

    # Explicit hydrogens for aromatic replacement rules
    mol_with_h = Chem.AddHs(mol)
    generated = {}

    for rule in MEDCHEM_RULES:
        rxn_smarts = f"({rule['smarts']})>>({rule['repl']})"
        try:
            rxn = AllChem.ReactionFromSmarts(rxn_smarts)
            # Try on mol or mol_with_h
            reactants = (mol_with_h,) if "[H]" in rule['smarts'] else (mol,)
            products = rxn.RunReactants(reactants)

            for prod_tuple in products:
                prod = prod_tuple[0]
                try:
                    prod = Chem.RemoveHs(prod)
                    Chem.SanitizeMol(prod)
                    smi = Chem.MolToSmiles(prod)
                    # Filter out identity or duplicates
                    if smi and smi != parent_smiles and smi not in generated:
                        generated[smi] = {
                            "smiles": smi,
                            "transformation": rule["name"],
                            "rationale": rule["rationale"]
                        }
                        if len(generated) >= max_analogs:
                            break
                except Exception:
                    continue
        except Exception:
            continue

        if len(generated) >= max_analogs:
            break

    return list(generated.values())


def evaluate_lead_optimization(
    parent_smiles: str,
    target_dataset: str = "esol",
    max_analogs: int = 12
) -> Dict[str, Any]:
    """
    Generate analogs and evaluate property shifts against the parent compound.
    """
    parent_admet = compute_admet_profile(parent_smiles)
    parent_pred = predict_ensemble(parent_smiles, target_dataset)
    parent_val = float(parent_pred["mean"][0])

    analogs = generate_analogs(parent_smiles, max_analogs=max_analogs)
    analog_results = []

    for item in analogs:
        smi = item["smiles"]
        try:
            admet = compute_admet_profile(smi)
            pred = predict_ensemble(smi, target_dataset)
            val = float(pred["mean"][0])
            delta = val - parent_val

            analog_results.append({
                "SMILES": smi,
                "Transformation": item["transformation"],
                "Rationale": item["rationale"],
                "Predicted Value": val,
                "Delta": delta,
                "QED": admet["descriptors"]["QED"],
                "MW": admet["descriptors"]["Molecular Weight"],
                "LogP": admet["descriptors"]["LogP"],
                "TPSA": admet["descriptors"]["TPSA"],
                "Lipinski Pass": admet["filters"]["Lipinski"]["passed"]
            })
        except Exception:
            continue

    return {
        "parent_smiles": parent_smiles,
        "parent_val": parent_val,
        "parent_qed": parent_admet["descriptors"]["QED"],
        "target_dataset": target_dataset,
        "analogs": analog_results
    }