#![forbid(unsafe_code)]
use chematic_core::Molecule;
use crate::descriptors::{
hba_count, hbd_count, logp_crippen, molecular_weight, rotatable_bond_count, tpsa,
num_aromatic_heterocycles,
};
use crate::esol::esol_solubility;
use crate::logd::logd_simple;
use crate::pka::{pka_acid, pka_base};
pub fn bbb_score(mol: &Molecule) -> f64 {
-0.0148 * tpsa(mol) + 0.152 * logp_crippen(mol) + 0.139
}
pub fn bbb_passes(mol: &Molecule) -> bool {
tpsa(mol) < 90.0 && molecular_weight(mol) < 400.0 && hbd_count(mol) <= 3
}
pub fn caco2_permeability(mol: &Molecule) -> f64 {
-0.1416 * tpsa(mol) + 0.6585 * logp_crippen(mol) - 0.5046
}
pub fn herg_risk_score(mol: &Molecule) -> f64 {
let logp = logp_crippen(mol);
let mw = molecular_weight(mol);
let has_basic_n = pka_base(mol).map(|p| p > 7.0).unwrap_or(false);
let mut score = 0.0_f64;
if has_basic_n {
score += 0.40; }
if logp > 4.0 {
score += 0.30;
} else if logp > 2.0 {
score += 0.15;
}
if mw > 400.0 {
score += 0.20;
} else if mw > 300.0 {
score += 0.10;
}
score.min(1.0)
}
pub fn cyp3a4_inhibition_risk(mol: &Molecule) -> f64 {
let mw = molecular_weight(mol);
let logp = logp_crippen(mol);
let het_ar = num_aromatic_heterocycles(mol);
let hba = hba_count(mol);
let mut score = 0.0_f64;
if mw > 500.0 {
score += 0.25;
} else if mw > 400.0 {
score += 0.15;
}
if logp > 4.0 {
score += 0.25;
} else if logp > 3.0 {
score += 0.15;
}
if het_ar >= 2 {
score += 0.30;
} else if het_ar == 1 {
score += 0.15;
}
if hba >= 6 {
score += 0.20;
} else if hba >= 4 {
score += 0.10;
}
score.min(1.0)
}
#[derive(Debug, Clone)]
pub struct AdmetProfile {
pub bbb_score: f64,
pub bbb_passes: bool,
pub caco2: f64,
pub herg_risk: f64,
pub cyp3a4_risk: f64,
pub pka_acid: Option<f64>,
pub pka_base: Option<f64>,
pub esol: f64,
pub logd74: f64,
pub mw: f64,
pub logp: f64,
pub tpsa: f64,
pub hbd: usize,
pub hba: usize,
pub rotatable_bonds: usize,
}
pub fn admet_profile(mol: &Molecule) -> AdmetProfile {
AdmetProfile {
bbb_score: bbb_score(mol),
bbb_passes: bbb_passes(mol),
caco2: caco2_permeability(mol),
herg_risk: herg_risk_score(mol),
cyp3a4_risk: cyp3a4_inhibition_risk(mol),
pka_acid: pka_acid(mol),
pka_base: pka_base(mol),
esol: esol_solubility(mol),
logd74: logd_simple(mol, 7.4),
mw: molecular_weight(mol),
logp: logp_crippen(mol),
tpsa: tpsa(mol),
hbd: hbd_count(mol),
hba: hba_count(mol),
rotatable_bonds: rotatable_bond_count(mol),
}
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn mol(s: &str) -> Molecule {
parse(s).unwrap()
}
#[test]
fn test_bbb_benzene_passes() {
let m = mol("c1ccccc1");
assert!(bbb_passes(&m), "benzene should pass BBB rules");
assert!(bbb_score(&m) > -1.0, "benzene should have positive logBB");
}
#[test]
fn test_bbb_aspirin_passes() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert!(bbb_passes(&m), "aspirin should pass BBB rules (MW=180, TPSA~63)");
}
#[test]
fn test_bbb_score_high_tpsa_fails() {
let m = mol("CN(C)C(=N)NC(=N)N"); let score = bbb_score(&m);
assert!(score < 0.0, "high-TPSA molecule should have logBB < 0, got {score:.3}");
}
#[test]
fn test_bbb_rule_metformin_fails() {
let m = mol("CN(C)C(=N)NC(=N)N");
let passes = bbb_passes(&m);
let _ = passes;
}
#[test]
fn test_caco2_nonpolar_high() {
let m = mol("CCCCCC"); let perm = caco2_permeability(&m);
assert!(perm > -5.5, "hexane should have high Caco-2 (logPCaco2 > -5.5), got {perm:.3}");
}
#[test]
fn test_caco2_polar_low() {
let m = mol("OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O");
let perm = caco2_permeability(&m);
assert!(perm < -5.5, "glucose should have low Caco-2 (logPCaco2 < -5.5), got {perm:.3}");
}
#[test]
fn test_caco2_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let perm = caco2_permeability(&m);
assert!(perm > -10.0 && perm < -5.0, "aspirin Caco-2 in range, got {perm:.3}");
}
#[test]
fn test_herg_basic_lipophilic_high() {
let m = mol("c1cc(ccc1C(=O)CCCCN2CCC(CC2)c3ccc(cc3)Cl)F");
let risk = herg_risk_score(&m);
assert!(risk > 0.5, "basic + lipophilic molecule should have high hERG risk, got {risk:.3}");
}
#[test]
fn test_herg_benzene_low() {
let m = mol("c1ccccc1");
let risk = herg_risk_score(&m);
assert!(risk < 0.4, "benzene has low hERG risk, got {risk:.3}");
}
#[test]
fn test_herg_score_range() {
let m = mol("CN1CCCCC1"); let risk = herg_risk_score(&m);
assert!((0.0..=1.0).contains(&risk), "hERG score must be in [0,1], got {risk}");
}
#[test]
fn test_cyp3a4_benzene_low() {
let m = mol("c1ccccc1");
let risk = cyp3a4_inhibition_risk(&m);
assert!(risk < 0.3, "benzene has low CYP3A4 risk, got {risk:.3}");
}
#[test]
fn test_cyp3a4_large_het_ar_high() {
let m = mol("c1cnc(nc1)-c1nc2ccccc2n1"); let risk = cyp3a4_inhibition_risk(&m);
assert!(risk > 0.0, "aromatic heterocycles have some CYP3A4 risk");
}
#[test]
fn test_cyp3a4_score_range() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let risk = cyp3a4_inhibition_risk(&m);
assert!((0.0..=1.0).contains(&risk), "CYP3A4 score in [0,1], got {risk}");
}
#[test]
fn test_admet_profile_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let profile = admet_profile(&m);
assert!(profile.mw > 170.0 && profile.mw < 185.0, "aspirin MW ~180");
assert!(profile.pka_acid.is_some(), "aspirin has acid site");
assert!(profile.bbb_passes, "aspirin passes BBB rules");
assert!(profile.herg_risk >= 0.0 && profile.herg_risk <= 1.0);
assert!(profile.cyp3a4_risk >= 0.0 && profile.cyp3a4_risk <= 1.0);
}
#[test]
fn test_admet_profile_benzene() {
let m = mol("c1ccccc1");
let profile = admet_profile(&m);
assert!(profile.pka_acid.is_none());
assert!(profile.pka_base.is_none());
assert!(profile.bbb_passes);
}
#[test]
fn test_admet_profile_glucose() {
let m = mol("OCC1OC(O)C(O)C(O)C1O");
let profile = admet_profile(&m);
assert!(!profile.bbb_passes, "glucose should not pass BBB rules");
assert!(profile.caco2 < -5.5, "glucose has low Caco-2 permeability");
}
}