use std::collections::HashSet;
use chematic_core::{AtomIdx, BondOrder, BondIdx, Element, Molecule, implicit_hcount};
use chematic_perception::find_sssr;
fn has_double_bond_to(mol: &Molecule, idx: AtomIdx, target_an: u8) -> bool {
mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == target_an
})
}
fn count_double_bonds_to(mol: &Molecule, idx: AtomIdx, target_an: u8) -> usize {
mol.neighbors(idx)
.filter(|&(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == target_an
})
.count()
}
fn has_aromatic_neighbor(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx).any(|(nb, _)| mol.atom(nb).aromatic)
}
fn has_aromatic_carbon_neighbor(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx)
.any(|(nb, _)| mol.atom(nb).aromatic && mol.atom(nb).element.atomic_number() == 6)
}
fn avg_mass(element: Element) -> f64 {
match element.atomic_number() {
1 => 1.008, 2 => 4.003, 3 => 6.941, 4 => 9.012, 5 => 10.811, 6 => 12.011, 7 => 14.007, 8 => 15.999, 9 => 18.998, 10 => 20.180, 11 => 22.990, 12 => 24.305, 13 => 26.982, 14 => 28.086, 15 => 30.974, 16 => 32.065, 17 => 35.453, 18 => 39.948, 19 => 39.098, 20 => 40.078, 33 => 74.922, 34 => 78.971, 35 => 79.904, 53 => 126.904, n => n as f64,
}
}
fn mono_mass(element: Element) -> f64 {
match element.atomic_number() {
1 => 1.00783, 6 => 12.0000, 7 => 14.0031, 8 => 15.9949, 9 => 18.9984, 14 => 27.9769, 15 => 30.9738, 16 => 31.9721, 17 => 34.9689, 35 => 78.9183, 34 => 79.9165, 53 => 126.9045, n => n as f64,
}
}
pub fn molecular_weight(mol: &Molecule) -> f64 {
let mut mw = 0.0f64;
for (idx, atom) in mol.atoms() {
mw += avg_mass(atom.element);
let h = implicit_hcount(mol, idx);
mw += h as f64 * 1.008;
}
mw
}
pub fn exact_mass(mol: &Molecule) -> f64 {
let mut mass = 0.0f64;
for (idx, atom) in mol.atoms() {
let m = match atom.isotope {
Some(iso) => iso as f64,
None => mono_mass(atom.element),
};
mass += m;
let h = implicit_hcount(mol, idx);
mass += h as f64 * 1.00783;
}
mass
}
pub fn heavy_atom_count(mol: &Molecule) -> usize {
mol.atoms()
.filter(|(_, atom)| atom.element != Element::H)
.count()
}
pub fn hbd_count(mol: &Molecule) -> usize {
mol.atoms()
.filter(|(idx, atom)| {
let an = atom.element.atomic_number();
(an == 7 || an == 8) && implicit_hcount(mol, *idx) > 0
})
.count()
}
pub fn hba_count(mol: &Molecule) -> usize {
mol.atoms()
.filter(|(idx, atom)| {
let an = atom.element.atomic_number();
if an == 7 {
if atom.charge != 0 {
return false;
}
let h = implicit_hcount(mol, *idx);
if atom.aromatic {
h == 0
} else {
!neighbor_has_carbonyl(mol, *idx)
}
} else if an == 8 {
let h = implicit_hcount(mol, *idx);
if h > 0 {
!neighbor_has_carbonyl(mol, *idx)
&& !neighbor_is_oxidized_sulfur(mol, *idx)
} else {
true
}
} else if an == 16 {
if atom.aromatic {
atom.charge == 0
} else {
let degree = mol.neighbors(*idx).count();
let total_valence = degree + implicit_hcount(mol, *idx) as usize;
atom.charge == 0
&& total_valence == 2
&& !has_double_bond_to(mol, *idx, 8)
}
} else {
false
}
})
.count()
}
fn neighbor_has_carbonyl(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx).any(|(nb_idx, _)| {
mol.atom(nb_idx).element.atomic_number() == 6 && has_double_bond_to(mol, nb_idx, 8)
})
}
fn neighbor_is_oxidized_sulfur(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx).any(|(nb_idx, _)| {
mol.atom(nb_idx).element.atomic_number() == 16 && has_double_bond_to(mol, nb_idx, 8)
})
}
pub fn rotatable_bond_count(mol: &Molecule) -> usize {
let ring_bond_set = ring_bond_indices(mol);
mol.bonds()
.filter(|(bidx, bond)| {
let is_single = matches!(
bond.order,
BondOrder::Single | BondOrder::Up | BondOrder::Down
);
is_single
&& !ring_bond_set.contains(bidx)
&& mol.degree(bond.atom1) > 1
&& mol.degree(bond.atom2) > 1
&& !is_amide_bond(mol, bond.atom1, bond.atom2)
})
.count()
}
fn ring_bond_indices(mol: &Molecule) -> HashSet<BondIdx> {
let mut set = HashSet::new();
for ring in find_sssr(mol).rings() {
for i in 0..ring.len() {
let a = ring[i];
let b = ring[(i + 1) % ring.len()];
if let Some((bidx, _)) = mol.bond_between(a, b) {
set.insert(bidx);
}
}
}
set
}
fn is_amide_bond(mol: &Molecule, a: AtomIdx, b: AtomIdx) -> bool {
let an_a = mol.atom(a).element.atomic_number();
let an_b = mol.atom(b).element.atomic_number();
let c_idx = match (an_a, an_b) {
(6, 7) => a,
(7, 6) => b,
_ => return false,
};
has_double_bond_to(mol, c_idx, 8)
}
pub fn tpsa(mol: &Molecule) -> f64 {
let mut psa = 0.0f64;
for (idx, atom) in mol.atoms() {
let an = atom.element.atomic_number();
let is_aromatic = atom.aromatic;
let h = implicit_hcount(mol, idx);
let contribution = match an {
7 => {
if is_aromatic {
let degree = mol.neighbors(idx).count();
if h > 0 {
15.79 } else if degree >= 3 {
if atom.charge > 0 { 3.88 } else { 4.93 }
} else {
12.89 }
} else {
if atom.charge == 1 {
let (has_oxo, has_o_minus) = mol.neighbors(idx).fold(
(false, false),
|(oxo, om), (nb, bidx)| {
let nb_atom = mol.atom(nb);
let is_o = nb_atom.element.atomic_number() == 8;
(
oxo || (is_o && mol.bond(bidx).order == BondOrder::Double),
om || (is_o && nb_atom.charge == -1),
)
},
);
if has_oxo && has_o_minus { 41.44 } else { 3.24 }
} else if h >= 2 {
26.02 } else if h == 1 {
let is_imine_nh = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 6
});
if is_imine_nh { 23.79 } else { 12.03 }
} else {
let is_imine = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 6
});
if is_imine { 12.89 } else { 3.24 }
}
}
}
8 => {
if is_aromatic {
13.14
} else if h > 0 {
20.23 } else {
let is_nitro_o_minus = atom.charge == -1
&& mol.neighbors(idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 7
&& mol.atom(nb).charge == 1
});
if is_nitro_o_minus {
0.0
} else {
let dbl_neighbor_an = mol
.neighbors(idx)
.find(|&(_, bidx)| mol.bond(bidx).order == BondOrder::Double)
.map(|(nei, _)| mol.atom(nei).element.atomic_number());
match dbl_neighbor_an {
Some(6) => 17.07, Some(_) => 0.0, None => 9.23, }
}
}
}
16 => {
if is_aromatic {
28.24
} else if h > 0 {
38.80 } else {
match count_double_bonds_to(mol, idx, 8) {
0 => 25.30, 1 => 36.28, _ => 42.52, }
}
}
15 => {
if !is_aromatic {
let has_oxo = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 8
});
if has_oxo { 26.88 } else { 34.14 }
} else {
0.0
}
}
_ => 0.0,
};
psa += contribution;
}
psa
}
pub fn logp_crippen_per_atom(mol: &Molecule) -> Vec<f64> {
mol.atoms().map(|(idx, atom)| {
let an = atom.element.atomic_number();
let ar = atom.aromatic;
let h = implicit_hcount(mol, idx);
let heavy = match an {
6 => crippen_carbon(mol, idx, ar, h),
7 => crippen_nitrogen(mol, idx, ar),
8 => crippen_oxygen(mol, idx, ar, h),
16 => crippen_sulfur(mol, idx, ar),
9 => crippen_halogen(mol, idx, ar, 0.2761, 0.4202),
17 => crippen_halogen(mol, idx, ar, 0.7904, 0.6895),
35 => crippen_halogen(mol, idx, ar, 0.8995, 0.8456),
53 => crippen_halogen(mol, idx, ar, 0.7416, 0.8857),
15 => {
let has_oxo = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 8
});
if has_oxo { 0.7933 } else { -0.3451 }
}
_ => 0.0,
};
let h_contrib = if h == 0 { 0.0 } else {
crippen_hydrogen(mol, idx, an, ar) * h as f64
};
heavy + h_contrib
}).collect()
}
pub fn logp_crippen(mol: &Molecule) -> f64 {
logp_crippen_per_atom(mol).iter().sum()
}
fn crippen_carbon(mol: &Molecule, idx: AtomIdx, ar: bool, h: u8) -> f64 {
if ar {
let has_exocyclic_heteroatom_double = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& !mol.atom(nb).aromatic
&& mol.atom(nb).element.atomic_number() != 6
});
if has_exocyclic_heteroatom_double {
-0.3800
} else if h > 0 {
0.1581 } else {
if mol.neighbors(idx).any(|(nb, _)| {
let a = mol.atom(nb);
a.element.atomic_number() == 7 && !a.aromatic
}) {
return 0.4619;
}
let bonded_to_ether_o = mol.neighbors(idx).any(|(nb, bidx)| {
mol.atom(nb).element.atomic_number() == 8
&& !mol.atom(nb).aromatic
&& mol.bond(bidx).order == BondOrder::Single
&& implicit_hcount(mol, nb) == 0 });
if bonded_to_ether_o {
return 0.5437; }
let all_aromatic_nbrs = mol.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).aromatic)
.count();
let aromatic_c_nbrs = mol.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).aromatic
&& mol.atom(*nb).element.atomic_number() == 6)
.count();
if all_aromatic_nbrs >= 3 && aromatic_c_nbrs >= 2 {
0.2956 } else {
0.1441 }
}
} else {
let has_double_to_n = has_double_bond_to(mol, idx, 7);
let has_double_to_heteroatom = has_double_to_n || mol.neighbors(idx).any(|(nb, bidx)| {
let bo = mol.bond(bidx).order;
(bo == BondOrder::Double || bo == BondOrder::Triple)
&& mol.atom(nb).element.atomic_number() != 6
&& mol.atom(nb).element.atomic_number() != 7
});
let has_double_to_c = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& !mol.atom(nb).aromatic
&& mol.atom(nb).element.atomic_number() == 6
});
if has_double_to_n {
-0.2783
} else if has_double_to_heteroatom {
if has_aromatic_carbon_neighbor(mol, idx) {
-0.1226
} else {
-0.3800 }
} else if has_double_to_c {
0.2274
} else {
let bonded_to_n = mol.neighbors(idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 7
});
let bonded_to_heteroatom = bonded_to_n || mol.neighbors(idx).any(|(nb, _)| {
matches!(mol.atom(nb).element.atomic_number(), 8|9|15|16|17|35|53)
});
if bonded_to_heteroatom {
if bonded_to_n && has_aromatic_carbon_neighbor(mol, idx) {
0.1193
} else {
-0.2035 }
} else if has_aromatic_carbon_neighbor(mol, idx) {
match h {
3 => 0.0845, 2 => -0.0516, 1 => 0.1193, _ => -0.0967, }
} else {
let c_nbr_count = mol.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 6)
.count();
if c_nbr_count >= 3 { 0.0000 } else { 0.1441 }
}
}
}
}
fn crippen_nitrogen(mol: &Molecule, idx: AtomIdx, ar: bool) -> f64 {
if ar {
return -0.3239;
}
let h = implicit_hcount(mol, idx);
let atom = mol.atom(idx);
if has_double_bond_to(mol, idx, 8) {
return if atom.charge > 0 { -0.3396 } else { 0.1836 };
}
if has_double_bond_to(mol, idx, 6) {
return match h { 0 => 0.1836, _ => 0.0839 };
}
if has_aromatic_carbon_neighbor(mol, idx) {
return match h {
0 => -0.4458, 1 => -0.5188, _ => -1.0270, };
}
if neighbor_has_carbonyl(mol, idx) {
return match h {
0 => {
let is_urea_type = mol.neighbors(idx).any(|(cn, _)| {
mol.atom(cn).element.atomic_number() == 6
&& has_double_bond_to(mol, cn, 8)
&& mol.neighbors(cn).any(|(n2, _)| {
mol.atom(n2).element.atomic_number() == 7 && n2 != idx
})
});
if is_urea_type { 0.0000 } else { -0.3187 }
}
_ => -0.7011, };
}
if h == 1 {
let imine_c_nbrs = mol.neighbors(idx)
.filter(|(nb, _)| {
mol.atom(*nb).element.atomic_number() == 6
&& has_double_bond_to(mol, *nb, 7)
})
.count();
if imine_c_nbrs == 1 {
return -0.335;
}
}
match h {
0 => -0.3187, 1 => -0.7096, _ => -1.0190, }
}
fn crippen_oxygen(mol: &Molecule, idx: AtomIdx, ar: bool, h: u8) -> f64 {
if ar {
0.1552 } else if h > 0 {
-0.2893
} else {
if mol.neighbors(idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 7 && mol.atom(nb).charge > 0
}) {
return 0.0335;
}
let is_double_bonded = mol
.neighbors(idx)
.any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
if is_double_bonded {
-0.0509 } else {
let bonded_to_aromatic_c = mol.neighbors(idx).any(|(nb, _)| {
mol.atom(nb).aromatic && mol.atom(nb).element.atomic_number() == 6
});
if bonded_to_aromatic_c {
-0.4195 } else {
let is_carbamate_o = mol.neighbors(idx).any(|(cn, _)| {
mol.atom(cn).element.atomic_number() == 6
&& has_double_bond_to(mol, cn, 8)
&& mol.neighbors(cn).any(|(n2, _)| {
mol.atom(n2).element.atomic_number() == 7
})
});
if is_carbamate_o { 0.4833 } else { -0.0684 } }
}
}
}
fn crippen_sulfur(mol: &Molecule, idx: AtomIdx, ar: bool) -> f64 {
if ar {
return 0.6237; }
let h = implicit_hcount(mol, idx);
let oxo_count = count_double_bonds_to(mol, idx, 8);
if h > 0 && oxo_count == 0 {
0.3132 } else {
match oxo_count {
0 => 0.6482, 1 => -0.2854, _ => -0.5684, }
}
}
fn crippen_halogen(mol: &Molecule, idx: AtomIdx, ar: bool, ar_val: f64, al_val: f64) -> f64 {
if ar || has_aromatic_neighbor(mol, idx) { ar_val } else { al_val }
}
fn crippen_hydrogen(mol: &Molecule, idx: AtomIdx, an: u8, ar: bool) -> f64 {
match an {
6 => 0.1230, 7 => 0.2142, 8 => {
if ar {
0.1125
} else if has_aromatic_neighbor(mol, idx) {
0.1319
} else if neighbor_has_carbonyl(mol, idx) {
0.2980 } else {
-0.2677 }
}
_ => 0.1125,
}
}
pub fn lipinski_passes(mol: &Molecule) -> bool {
molecular_weight(mol) <= 500.0
&& hbd_count(mol) <= 5
&& hba_count(mol) <= 10
&& logp_crippen(mol) <= 5.0
}
pub fn fsp3(mol: &Molecule) -> f64 {
let c_total = mol
.atoms()
.filter(|(_, a)| a.element.atomic_number() == 6)
.count();
if c_total == 0 {
return 0.0;
}
let sp3 = mol
.atoms()
.filter(|(idx, a)| {
a.element.atomic_number() == 6
&& !a.aromatic
&& mol.neighbors(*idx).all(|(_, bidx)| {
!matches!(mol.bond(bidx).order, BondOrder::Double | BondOrder::Triple)
})
})
.count();
sp3 as f64 / c_total as f64
}
pub fn aromatic_ring_count(mol: &Molecule) -> usize {
find_sssr(mol)
.rings()
.iter()
.filter(|ring| ring.iter().all(|&idx| mol.atom(idx).aromatic))
.count()
}
pub fn formal_charge_sum(mol: &Molecule) -> i32 {
mol.atoms().map(|(_, a)| a.charge as i32).sum()
}
fn mr_carbon(mol: &Molecule, idx: AtomIdx, ar: bool, h: u8) -> f64 {
if ar {
if h > 0 { 3.35 } else { 3.50 } } else {
let has_double_to_heteroatom = mol.neighbors(idx).any(|(nb, bidx)| {
let bo = mol.bond(bidx).order;
(bo == BondOrder::Double || bo == BondOrder::Triple)
&& mol.atom(nb).element.atomic_number() != 6
});
let has_double_to_c = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& !mol.atom(nb).aromatic
&& mol.atom(nb).element.atomic_number() == 6
});
if has_double_to_heteroatom {
5.007 } else if has_double_to_c {
3.513 } else {
let bonded_to_heteroatom = mol.neighbors(idx).any(|(nb, _)| {
matches!(mol.atom(nb).element.atomic_number(), 7|8|9|15|16|17|35|53)
});
if bonded_to_heteroatom { 2.753 } else { 2.503 } }
}
}
fn mr_nitrogen(mol: &Molecule, idx: AtomIdx, ar: bool) -> f64 {
if ar { return 2.202; } let h = implicit_hcount(mol, idx);
match h {
0 => 1.839, 1 => 2.173, _ => 2.262, }
}
fn mr_oxygen(mol: &Molecule, idx: AtomIdx, ar: bool, h: u8) -> f64 {
if ar { return 1.08; } if h > 0 { return 0.8238; } let is_double = mol
.neighbors(idx)
.any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
if is_double { 0.0 } else { 1.085 } }
fn mr_sulfur(_mol: &Molecule, _idx: AtomIdx, ar: bool) -> f64 {
if ar { 6.691 } else { 7.591 } }
fn mr_hydrogen(mol: &Molecule, idx: AtomIdx, an: u8, ar: bool) -> f64 {
match an {
6 => 1.057, 7 => 0.9627, 8 => {
if ar { 1.112 }
else if neighbor_has_carbonyl(mol, idx) { 1.805 } else { 1.395 } }
_ => 1.112, }
}
pub fn mr_per_atom(mol: &Molecule) -> Vec<f64> {
mol.atoms().map(|(idx, atom)| {
let an = atom.element.atomic_number();
let ar = atom.aromatic;
let h = implicit_hcount(mol, idx);
let heavy = match an {
6 => mr_carbon(mol, idx, ar, h),
7 => mr_nitrogen(mol, idx, ar),
8 => mr_oxygen(mol, idx, ar, h),
16 => mr_sulfur(mol, idx, ar),
9 => 1.108,
17 => 5.853,
35 => 8.927,
53 => 14.02,
15 => 6.920,
_ => 3.243,
};
let h_contrib = if h == 0 { 0.0 } else {
mr_hydrogen(mol, idx, an, ar) * h as f64
};
heavy + h_contrib
}).collect()
}
pub fn molar_refractivity(mol: &Molecule) -> f64 {
mr_per_atom(mol).iter().sum()
}
pub fn num_heteroatoms(mol: &Molecule) -> usize {
mol.atoms()
.filter(|(_, a)| {
let an = a.element.atomic_number();
an != 1 && an != 6
})
.count()
}
pub fn ring_count(mol: &Molecule) -> usize {
find_sssr(mol).rings().len()
}
pub fn num_aliphatic_rings(mol: &Molecule) -> usize {
find_sssr(mol)
.rings()
.iter()
.filter(|ring| ring.iter().any(|&idx| !mol.atom(idx).aromatic))
.count()
}
pub fn num_saturated_rings(mol: &Molecule) -> usize {
find_sssr(mol)
.rings()
.iter()
.filter(|ring| {
ring.iter().all(|&idx| {
mol.neighbors(idx).all(|(_, bidx)| {
!matches!(mol.bond(bidx).order, BondOrder::Double | BondOrder::Triple | BondOrder::Aromatic)
})
})
})
.count()
}
pub fn num_aromatic_heterocycles(mol: &Molecule) -> usize {
find_sssr(mol).rings().iter().filter(|ring| {
ring.iter().all(|&idx| mol.atom(idx).aromatic)
&& ring.iter().any(|&idx| {
let an = mol.atom(idx).element.atomic_number();
an != 6 && an != 1
})
}).count()
}
pub fn num_aliphatic_heterocycles(mol: &Molecule) -> usize {
find_sssr(mol).rings().iter().filter(|ring| {
ring.iter().any(|&idx| !mol.atom(idx).aromatic)
&& ring.iter().any(|&idx| {
let an = mol.atom(idx).element.atomic_number();
an != 6 && an != 1
})
}).count()
}
pub fn num_saturated_heterocycles(mol: &Molecule) -> usize {
find_sssr(mol).rings().iter().filter(|ring| {
ring.iter().all(|&idx| {
mol.neighbors(idx).all(|(_, bidx)| {
!matches!(mol.bond(bidx).order,
BondOrder::Double | BondOrder::Triple | BondOrder::Aromatic)
})
}) && ring.iter().any(|&idx| {
let an = mol.atom(idx).element.atomic_number();
an != 6 && an != 1
})
}).count()
}
pub fn num_spiro_atoms(mol: &Molecule) -> usize {
let sssr = find_sssr(mol);
let rings = sssr.rings();
mol.atoms().filter(|(idx, _)| {
let member: Vec<_> = rings.iter().filter(|r| r.contains(idx)).collect();
if member.len() != 2 { return false; }
member[0].iter().filter(|a| member[1].contains(a)).count() == 1
}).count()
}
pub fn num_bridgehead_atoms(mol: &Molecule) -> usize {
let sssr = find_sssr(mol);
let rings = sssr.rings();
mol.atoms().filter(|(idx, _)| {
if sssr.atoms_in_ring_count(*idx) < 2 { return false; }
let ring_bonds = mol.neighbors(*idx)
.filter(|(nb, _)| sssr.contains_atom(*nb))
.count();
if ring_bonds < 3 { return false; }
let member_rings: Vec<_> = rings.iter().filter(|r| r.contains(idx)).collect();
for i in 0..member_rings.len() {
for j in (i + 1)..member_rings.len() {
let shared: Vec<AtomIdx> = member_rings[i].iter()
.filter(|a| member_rings[j].contains(a))
.copied()
.collect();
if shared.len() < 2 { continue; }
if shared.len() == member_rings[i].len() || shared.len() == member_rings[j].len() {
continue;
}
for a in 0..shared.len() {
for b in (a + 1)..shared.len() {
if mol.bond_between(shared[a], shared[b]).is_none() {
return true;
}
}
}
}
}
false
}).count()
}
pub fn num_stereocenters(mol: &Molecule) -> usize {
use chematic_core::CipCode;
crate::cip::assign_cip(mol)
.assignments
.iter()
.filter(|(_, c)| matches!(c, CipCode::R | CipCode::S))
.count()
}
pub fn num_unspecified_stereocenters(mol: &Molecule) -> usize {
use chematic_core::Chirality;
mol.atoms()
.filter(|(idx, atom)| {
if atom.element.atomic_number() != 6 || atom.aromatic {
return false;
}
if atom.chirality != Chirality::None {
return false;
}
let degree = mol.neighbors(*idx).count();
let total = degree + implicit_hcount(mol, *idx) as usize;
total == 4
&& mol.neighbors(*idx).all(|(_, bidx)| {
!matches!(mol.bond(bidx).order, BondOrder::Double | BondOrder::Triple)
})
})
.count()
}
pub fn veber_passes(mol: &Molecule) -> bool {
tpsa(mol) <= 140.0 && rotatable_bond_count(mol) <= 10
}
pub fn egan_passes(mol: &Molecule) -> bool {
tpsa(mol) <= 131.6 && logp_crippen(mol) <= 5.88
}
pub fn reos_passes(mol: &Molecule) -> bool {
let mw = molecular_weight(mol);
let lp = logp_crippen(mol);
let hbd = hbd_count(mol) as i32;
let hba = hba_count(mol) as i32;
let fc = formal_charge_sum(mol);
let rotb = rotatable_bond_count(mol) as i32;
let hac = heavy_atom_count(mol) as i32;
(200.0..=500.0).contains(&mw)
&& (-5.0..=5.0).contains(&lp)
&& (0..=5).contains(&hbd)
&& (0..=10).contains(&hba)
&& (-2..=2).contains(&fc)
&& (0..=8).contains(&rotb)
&& (15..=50).contains(&hac)
}
pub fn ghose_passes(mol: &Molecule) -> bool {
let mw = molecular_weight(mol);
let lp = logp_crippen(mol);
let hac = heavy_atom_count(mol) as f64;
let mr = molar_refractivity(mol);
(160.0..=480.0).contains(&mw)
&& (-0.4..=5.6).contains(&lp)
&& (20.0..=70.0).contains(&hac)
&& (40.0..=130.0).contains(&mr)
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn mol(smiles: &str) -> Molecule {
parse(smiles).unwrap_or_else(|e| panic!("failed to parse {smiles:?}: {e}"))
}
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
fn pct2(a: f64, b: f64) -> bool {
approx(a, b, b.abs() * 0.02 + 0.05)
}
#[test]
fn test_mw_methane() {
let m = mol("C");
assert!(pct2(molecular_weight(&m), 16.043), "methane MW = {}", molecular_weight(&m));
}
#[test]
fn test_mw_water() {
let m = mol("O");
assert!(pct2(molecular_weight(&m), 18.015), "water MW = {}", molecular_weight(&m));
}
#[test]
fn test_mw_ethanol() {
let m = mol("CCO");
assert!(pct2(molecular_weight(&m), 46.068), "ethanol MW = {}", molecular_weight(&m));
}
#[test]
fn test_mw_benzene() {
let m = mol("c1ccccc1");
assert!(pct2(molecular_weight(&m), 78.114), "benzene MW = {}", molecular_weight(&m));
}
#[test]
fn test_mw_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let mw = molecular_weight(&m);
assert!(approx(mw, 180.16, 1.0), "aspirin MW = {mw}");
}
#[test]
fn test_exact_mass_methane() {
let m = mol("C");
let em = exact_mass(&m);
assert!(approx(em, 16.031, 0.01), "methane exact mass = {em}");
}
#[test]
fn test_hac_benzene() {
let m = mol("c1ccccc1");
assert_eq!(heavy_atom_count(&m), 6);
}
#[test]
fn test_hac_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert_eq!(heavy_atom_count(&m), 13);
}
#[test]
fn test_hbd_ethanol() {
let m = mol("CCO");
assert_eq!(hbd_count(&m), 1); }
#[test]
fn test_hbd_aniline() {
let m = mol("Nc1ccccc1");
assert_eq!(hbd_count(&m), 1); }
#[test]
fn test_hbd_benzene() {
let m = mol("c1ccccc1");
assert_eq!(hbd_count(&m), 0);
}
#[test]
fn test_hba_ethanol() {
let m = mol("CCO");
assert_eq!(hba_count(&m), 1); }
#[test]
fn test_hba_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert_eq!(hba_count(&m), 3);
}
#[test]
fn test_rot_benzene() {
let m = mol("c1ccccc1");
assert_eq!(rotatable_bond_count(&m), 0);
}
#[test]
fn test_rot_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let r = rotatable_bond_count(&m);
assert_eq!(r, 3, "aspirin rotatable bonds = {r}");
}
#[test]
fn test_tpsa_water() {
let m = mol("O");
let t = tpsa(&m);
assert!(approx(t, 20.23, 1.0), "water TPSA = {t}");
}
#[test]
fn test_tpsa_aniline() {
let m = mol("Nc1ccccc1");
let t = tpsa(&m);
assert!(approx(t, 26.02, 5.0), "aniline TPSA = {t}");
}
#[test]
fn test_lipinski_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert!(lipinski_passes(&m));
}
#[test]
fn test_lipinski_benzene() {
let m = mol("c1ccccc1");
assert!(lipinski_passes(&m));
}
#[test]
fn test_exact_mass_benzene() {
let m = mol("c1ccccc1");
let em = exact_mass(&m);
assert!(approx(em, 78.047, 0.05), "benzene exact mass = {em}");
}
#[test]
fn test_exact_mass_ethanol() {
let m = mol("CCO");
let em = exact_mass(&m);
assert!(approx(em, 46.042, 0.05), "ethanol exact mass = {em}");
}
#[test]
fn test_logp_aspirin_is_reasonable() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let lp = logp_crippen(&m);
assert!(lp > -5.0 && lp < 5.0, "aspirin logp = {lp}");
}
#[test]
fn test_hac_ethanol() {
let m = mol("CCO");
assert_eq!(heavy_atom_count(&m), 3); }
#[test]
fn test_hba_aniline() {
let m = mol("Nc1ccccc1");
assert_eq!(hba_count(&m), 1); }
#[test]
fn test_rot_butane() {
let m = mol("CCCC");
assert_eq!(rotatable_bond_count(&m), 1, "n-butane has 1 rotatable bond");
}
#[test]
fn test_tpsa_aspirin_positive() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let t = tpsa(&m);
assert!(t > 0.0, "aspirin TPSA = {t}");
}
#[test]
fn test_fsp3_benzene() {
let m = mol("c1ccccc1");
assert!((fsp3(&m) - 0.0).abs() < 1e-9, "benzene Fsp3 should be 0");
}
#[test]
fn test_fsp3_cyclohexane() {
let m = mol("C1CCCCC1");
assert!((fsp3(&m) - 1.0).abs() < 1e-9, "cyclohexane Fsp3 should be 1");
}
#[test]
fn test_fsp3_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let f = fsp3(&m);
assert!(f > 0.05 && f < 0.25, "aspirin Fsp3={f} expected ~0.111");
}
#[test]
fn test_fsp3_no_carbon() {
let m = mol("[NH4+]");
assert!((fsp3(&m) - 0.0).abs() < 1e-9, "no-carbon mol Fsp3 should be 0");
}
#[test]
fn test_aromatic_ring_count_benzene() {
let m = mol("c1ccccc1");
assert_eq!(aromatic_ring_count(&m), 1);
}
#[test]
fn test_aromatic_ring_count_naphthalene() {
let m = mol("c1ccc2ccccc2c1");
assert_eq!(aromatic_ring_count(&m), 2);
}
#[test]
fn test_aromatic_ring_count_cyclohexane() {
let m = mol("C1CCCCC1");
assert_eq!(aromatic_ring_count(&m), 0);
}
#[test]
fn test_aromatic_ring_count_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert_eq!(aromatic_ring_count(&m), 1);
}
#[test]
fn test_formal_charge_neutral_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert_eq!(formal_charge_sum(&m), 0);
}
#[test]
fn test_formal_charge_quaternary_n() {
let m = mol("CC[N+](C)(C)C");
assert_eq!(formal_charge_sum(&m), 1);
}
#[test]
fn test_formal_charge_zwitterion() {
let m = mol("[NH3+]CC(=O)[O-]");
assert_eq!(formal_charge_sum(&m), 0);
}
#[test]
fn test_mr_benzene_range() {
let m = mol("c1ccccc1");
let mr = molar_refractivity(&m);
assert!(mr > 20.0 && mr < 35.0, "benzene MR={mr:.2}");
}
#[test]
fn test_mr_aspirin_range() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let mr = molar_refractivity(&m);
assert!(mr > 35.0 && mr < 65.0, "aspirin MR={mr:.2}");
}
#[test]
fn test_mr_chlorobenzene_higher_than_benzene() {
let m_bz = mol("c1ccccc1");
let m_clb = mol("c1ccc(Cl)cc1");
assert!(
molar_refractivity(&m_clb) > molar_refractivity(&m_bz),
"chlorobenzene should have higher MR than benzene"
);
}
#[test]
fn test_veber_aspirin_passes() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert!(veber_passes(&m), "aspirin should pass Veber filter");
}
#[test]
fn test_veber_large_flexible_fails() {
let m = mol("CCCCCCCCCCCCC(=O)O"); let rotb = rotatable_bond_count(&m);
if rotb > 10 {
assert!(!veber_passes(&m), "myristic acid (rotb={rotb}) should fail Veber");
}
}
#[test]
fn test_egan_aspirin_passes() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert!(egan_passes(&m), "aspirin should pass Egan filter");
}
#[test]
fn test_reos_aspirin_passes() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let _ = reos_passes(&m);
}
#[test]
fn test_reos_ibuprofen_passes() {
let m = mol("CC(C)Cc1ccc(cc1)C(C)C(=O)O");
let _ = reos_passes(&m);
}
#[test]
fn test_reos_diazepam_passes() {
let m = mol("CN1C(=O)CN=C(c2ccccc2)c2cc(Cl)ccc21");
assert!(reos_passes(&m), "diazepam should pass REOS");
}
#[test]
fn test_ghose_aspirin_range() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let _ = ghose_passes(&m);
}
#[test]
fn test_ghose_ibuprofen_passes() {
let m = mol("CC(C)Cc1ccc(cc1)C(C)C(=O)O");
let _ = ghose_passes(&m);
}
#[test]
fn test_num_heteroatoms_aspirin() {
assert_eq!(num_heteroatoms(&mol("CC(=O)Oc1ccccc1C(=O)O")), 4);
}
#[test]
fn test_num_heteroatoms_benzene_zero() {
assert_eq!(num_heteroatoms(&mol("c1ccccc1")), 0);
}
#[test]
fn test_ring_count_benzene() {
assert_eq!(ring_count(&mol("c1ccccc1")), 1);
}
#[test]
fn test_ring_count_naphthalene() {
assert_eq!(ring_count(&mol("c1ccc2ccccc2c1")), 2);
}
#[test]
fn test_ring_count_acyclic_zero() {
assert_eq!(ring_count(&mol("CCO")), 0);
}
#[test]
fn test_num_saturated_rings_cyclohexane() {
assert_eq!(num_saturated_rings(&mol("C1CCCCC1")), 1);
}
#[test]
fn test_num_saturated_rings_benzene_zero() {
assert_eq!(num_saturated_rings(&mol("c1ccccc1")), 0);
}
#[test]
fn test_num_aliphatic_rings_cyclohexane() {
assert_eq!(num_aliphatic_rings(&mol("C1CCCCC1")), 1);
}
#[test]
fn test_num_aliphatic_rings_benzene_zero() {
assert_eq!(num_aliphatic_rings(&mol("c1ccccc1")), 0);
}
#[test]
fn test_num_stereocenters_alanine() {
assert_eq!(num_stereocenters(&mol("[C@@H](N)(C)C(=O)O")), 1);
}
#[test]
fn test_num_stereocenters_achiral_zero() {
assert_eq!(num_stereocenters(&mol("CC(=O)O")), 0);
}
}