BioGraph-AI / src /analogs.py
VSRAJHAVEL's picture
Upload folder using huggingface_hub
2fd4f2f verified
Raw History Blame Contribute Delete
5.72 kB
"""
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
}