use crate::descriptors::logp_crippen;
use crate::pka::{pka_acid, pka_base};
use chematic_core::{Molecule, implicit_hcount};
const PKA_ACID: f64 = 4.0;
const PKA_BASE: f64 = 9.5;
fn ionisation_class(mol: &Molecule) -> (bool, bool) {
let mut acid_sites = 0u32; let mut base_sites = 0u32;
for (idx, atom) in mol.atoms() {
let an = atom.element.atomic_number();
let h = implicit_hcount(mol, idx) as u32;
match an {
8 if h > 0 => {
let has_carbonyl = mol.neighbors(idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 6
&& mol.neighbors(nb).any(|(nb2, bid2)| {
nb2 != idx
&& mol.atom(nb2).element.atomic_number() == 8
&& mol.bond(bid2).order == chematic_core::BondOrder::Double
})
});
if has_carbonyl {
acid_sites += 1;
}
}
16 if h > 0 => {
acid_sites += 1;
}
7 if atom.charge >= 0 => {
let heavy_deg = mol.neighbors(idx).count() as u32;
if heavy_deg <= 3 && !atom.aromatic {
base_sites += 1;
}
}
_ => {}
}
}
(acid_sites > 0, base_sites > 0)
}
fn hh_correction(logp: f64, ph: f64, pka: f64, is_acid: bool) -> f64 {
if is_acid {
logp - (1.0 + 10f64.powf(ph - pka)).log10()
} else {
logp - (1.0 + 10f64.powf(pka - ph)).log10()
}
}
pub fn logd_simple(mol: &Molecule, ph: f64) -> f64 {
let logp = logp_crippen(mol);
logd_from_logp(logp, mol, ph)
}
pub fn logd_from_logp(logp: f64, mol: &Molecule, ph: f64) -> f64 {
let (is_acid_legacy, is_base_legacy) = ionisation_class(mol);
let pka_a = pka_acid(mol).unwrap_or(PKA_ACID);
let pka_b = pka_base(mol).unwrap_or(PKA_BASE);
let is_acid = is_acid_legacy || pka_acid(mol).is_some();
let is_base = is_base_legacy || pka_base(mol).is_some();
match (is_acid, is_base) {
(true, false) => hh_correction(logp, ph, pka_a, true),
(false, true) => hh_correction(logp, ph, pka_b, false),
(true, true) => {
let acid_corr = hh_correction(logp, ph, pka_a, true);
let base_corr = hh_correction(logp, ph, pka_b, false);
(acid_corr + base_corr) / 2.0
}
(false, false) => logp,
}
}
pub fn logd_profile(mol: &Molecule, ph_start: f64, ph_end: f64, steps: usize) -> Vec<(f64, f64)> {
if steps == 0 {
return Vec::new();
}
if steps == 1 {
let ph = (ph_start + ph_end) / 2.0;
return vec![(ph, logd_simple(mol, ph))];
}
let step_size = (ph_end - ph_start) / (steps - 1) as f64;
(0..steps)
.map(|i| {
let ph = ph_start + i as f64 * step_size;
(ph, logd_simple(mol, ph))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn mol(s: &str) -> chematic_core::Molecule {
parse(s).unwrap()
}
#[test]
fn test_neutral_logd_equals_logp() {
let m = mol("c1ccccc1");
let logp = logp_crippen(&m);
let logd = logd_simple(&m, 7.4);
assert!(
(logd - logp).abs() < 0.01,
"neutral: logD={logd:.3} logP={logp:.3}"
);
}
#[test]
fn test_acid_logd_lower_at_high_ph() {
let m = mol("CC(=O)O");
let logp = logp_crippen(&m);
let logd_acid = logd_simple(&m, 9.0); assert!(
logd_acid < logp,
"acid at high pH: logD={logd_acid:.3} should be < logP={logp:.3}"
);
}
#[test]
fn test_logd_profile_length() {
let m = mol("CC");
let profile = logd_profile(&m, 1.0, 14.0, 5);
assert_eq!(profile.len(), 5);
assert!((profile[0].0 - 1.0).abs() < 1e-9);
assert!((profile[4].0 - 14.0).abs() < 1e-9);
}
#[test]
fn test_logd_profile_zero_steps() {
let m = mol("CC");
assert!(logd_profile(&m, 0.0, 14.0, 0).is_empty());
}
}