BioGraph-AI / src /admet.py
VSRAJHAVEL's picture
Upload folder using huggingface_hub
2fd4f2f verified
Raw History Blame Contribute Delete
6.74 kB
"""
ADMET & In Silico Drug-Likeness Profiling Engine.
Computes physicochemical descriptors, Lipinski Rule of 5, Veber, Ghose, Egan,
QED, PAINS filter alerts, Brenk toxicophores, and Bioavailability Radar coordinates.
"""
from typing import Dict, Any, List, Tuple, Optional
import numpy as np
from rdkit import Chem
from rdkit.Chem import Descriptors, Lipinski, Crippen, QED
from rdkit.Chem.FilterCatalog import FilterCatalog, FilterCatalogParams
# Initialize FilterCatalogs for PAINS and BRENK structural alerts
_pains_params = FilterCatalogParams()
_pains_params.AddCatalog(FilterCatalogParams.FilterCatalogs.PAINS)
PAINS_CATALOG = FilterCatalog(_pains_params)
_brenk_params = FilterCatalogParams()
_brenk_params.AddCatalog(FilterCatalogParams.FilterCatalogs.BRENK)
BRENK_CATALOG = FilterCatalog(_brenk_params)
def compute_admet_profile(mol_or_smiles: Any) -> Dict[str, Any]:
"""
Compute comprehensive in silico ADMET and drug-likeness profile.
Args:
mol_or_smiles: RDKit Mol object or SMILES string.
Returns:
Dictionary containing descriptors, rule evaluations, alerts, and scores.
"""
if isinstance(mol_or_smiles, str):
mol = Chem.MolFromSmiles(mol_or_smiles)
else:
mol = mol_or_smiles
if mol is None:
raise ValueError("Invalid molecule provided for ADMET calculation.")
# 1. Fundamental Physicochemical Descriptors
mw = float(Descriptors.MolWt(mol))
logp = float(Crippen.MolLogP(mol))
tpsa = float(Descriptors.TPSA(mol))
hbd = int(Lipinski.NumHDonors(mol))
hba = int(Lipinski.NumHAcceptors(mol))
rot_bonds = int(Lipinski.NumRotatableBonds(mol))
heavy_atoms = int(mol.GetNumHeavyAtoms())
aromatic_rings = int(Lipinski.NumAromaticRings(mol))
fraction_csp3 = float(Descriptors.FractionCSP3(mol))
molar_refractivity = float(Crippen.MolMR(mol))
qed_score = float(QED.qed(mol))
# Synthetic Accessibility Approximation (heuristic based on complexity, rings, chiral centers)
num_chiral = len(Chem.FindMolChiralCenters(mol, includeUnassigned=True))
num_rings = Descriptors.RingCount(mol)
bertz = Descriptors.BertzCT(mol)
# Normalized complexity heuristic between 1.0 (very easy) and 10.0 (very hard)
sascore_est = min(10.0, max(1.0, 1.0 + (bertz / 250.0) + (num_chiral * 0.4) + (max(0, num_rings - 2) * 0.5)))
# 2. Rule of 5 (Lipinski)
lipinski_violations = []
if mw > 500:
lipinski_violations.append(f"MW > 500 ({mw:.1f} Da)")
if logp > 5.0:
lipinski_violations.append(f"LogP > 5.0 ({logp:.2f})")
if hbd > 5:
lipinski_violations.append(f"HBD > 5 ({hbd})")
if hba > 10:
lipinski_violations.append(f"HBA > 10 ({hba})")
lipinski_pass = len(lipinski_violations) <= 1
# 3. Veber Rule (Oral Bioavailability)
veber_violations = []
if rot_bonds > 10:
veber_violations.append(f"Rotatable Bonds > 10 ({rot_bonds})")
if tpsa > 140.0:
veber_violations.append(f"TPSA > 140 Ų ({tpsa:.1f})")
veber_pass = len(veber_violations) == 0
# 4. Ghose Filter
ghose_violations = []
if not (160 <= mw <= 480):
ghose_violations.append(f"MW not in [160, 480] ({mw:.1f})")
if not (-0.4 <= logp <= 5.6):
ghose_violations.append(f"LogP not in [-0.4, 5.6] ({logp:.2f})")
if not (40 <= molar_refractivity <= 130):
ghose_violations.append(f"MR not in [40, 130] ({molar_refractivity:.1f})")
if not (20 <= heavy_atoms <= 70):
ghose_violations.append(f"Heavy atoms not in [20, 70] ({heavy_atoms})")
ghose_pass = len(ghose_violations) == 0
# 5. Egan Filter (Absorption)
egan_violations = []
if logp > 5.88:
egan_violations.append(f"LogP > 5.88 ({logp:.2f})")
if tpsa > 131.6:
egan_violations.append(f"TPSA > 131.6 ({tpsa:.1f})")
egan_pass = len(egan_violations) == 0
# 6. PAINS Filter (Pan-Assay Interference Compounds)
pains_matches = []
for entry in PAINS_CATALOG.GetMatches(mol):
pains_matches.append(entry.GetDescription())
# 7. Brenk Filter (Toxicophores & Reactive Groups)
brenk_matches = []
for entry in BRENK_CATALOG.GetMatches(mol):
brenk_matches.append(entry.GetDescription())
# 8. Bioavailability Radar Coordinates (Normalized to [0, 1] relative to drug-like box)
# Axes: LIPO (LogP), SIZE (MW), POLAR (TPSA), INSOLU (LogS proxy/LogP), INSATU (FractionCsp3), FLEX (RotBonds)
# Optimal ranges:
# LIPO: -0.7 to 5.0
# SIZE: 150 to 500
# POLAR: 20 to 130
# INSOLU: 0 to 6
# INSATU: 0.25 to 1.0
# FLEX: 0 to 9
radar_coords = {
"LIPO": float(np.clip((logp - (-0.7)) / (5.0 - (-0.7)), 0.05, 1.2)),
"SIZE": float(np.clip((mw - 150) / (500 - 150), 0.05, 1.2)),
"POLAR": float(np.clip((tpsa - 20) / (130 - 20), 0.05, 1.2)),
"INSOLU": float(np.clip(max(0, logp) / 6.0, 0.05, 1.2)),
"INSATU": float(np.clip(fraction_csp3, 0.05, 1.2)),
"FLEX": float(np.clip(rot_bonds / 9.0, 0.05, 1.2))
}
return {
"descriptors": {
"Molecular Weight": mw,
"LogP": logp,
"TPSA": tpsa,
"H-Bond Donors": hbd,
"H-Bond Acceptors": hba,
"Rotatable Bonds": rot_bonds,
"Heavy Atoms": heavy_atoms,
"Aromatic Rings": aromatic_rings,
"Fraction Csp3": fraction_csp3,
"Molar Refractivity": molar_refractivity,
"QED": qed_score,
"SA Score (est)": sascore_est
},
"filters": {
"Lipinski": {
"passed": lipinski_pass,
"num_violations": len(lipinski_violations),
"violations": lipinski_violations
},
"Veber": {
"passed": veber_pass,
"num_violations": len(veber_violations),
"violations": veber_violations
},
"Ghose": {
"passed": ghose_pass,
"num_violations": len(ghose_violations),
"violations": ghose_violations
},
"Egan": {
"passed": egan_pass,
"num_violations": len(egan_violations),
"violations": egan_violations
}
},
"alerts": {
"PAINS": {
"has_alerts": len(pains_matches) > 0,
"count": len(pains_matches),
"descriptions": pains_matches
},
"Brenk": {
"has_alerts": len(brenk_matches) > 0,
"count": len(brenk_matches),
"descriptions": brenk_matches
}
},
"radar": radar_coords
}