use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::OnceLock;
use chematic_core::{
AtomIdx, BondIdx, BondOrder, Element, Molecule, bond_order_sum, implicit_hcount,
};
use chematic_perception::{
all_ring_list, aromatic_ring_list, find_ring_families, find_sssr, ring_bonds_all_aromatic,
};
use chematic_smarts::{MatchConfig, find_matches_with_rings_and_config, parse_smarts};
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 is_atom_in_ring(mol: &Molecule, idx: AtomIdx) -> bool {
let nbs: Vec<AtomIdx> = mol.neighbors(idx).map(|(nb, _)| nb).collect();
if nbs.len() < 2 {
return false;
}
for start_i in 0..nbs.len() {
let start = nbs[start_i];
let targets: FxHashSet<AtomIdx> = nbs
.iter()
.enumerate()
.filter(|(j, _)| *j != start_i)
.map(|(_, &nb)| nb)
.collect();
let mut visited = FxHashSet::default();
visited.insert(idx);
visited.insert(start);
let mut queue = std::collections::VecDeque::new();
queue.push_back(start);
while let Some(curr) = queue.pop_front() {
if targets.contains(&curr) {
return true;
}
for (nb, _) in mol.neighbors(curr) {
if visited.insert(nb) {
queue.push_back(nb);
}
}
}
}
false
}
fn is_aromatic_oxide_bridge(mol: &Molecule, idx: AtomIdx) -> bool {
let has_aromatic_c_nb = mol.neighbors(idx).any(|(nb, _)| {
let a = mol.atom(nb);
a.aromatic && a.element.atomic_number() == 6
});
if !has_aromatic_c_nb {
return false;
}
let has_vinyl_c_nb = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order != BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 6
&& mol.neighbors(nb).any(|(nb2, b2)| {
nb2 != idx
&& mol.bond(b2).order == BondOrder::Double
&& mol.atom(nb2).element.atomic_number() == 6
})
});
has_vinyl_c_nb && is_atom_in_ring(mol, idx)
}
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()
}
#[inline]
fn is_carbon(an: u8) -> bool {
an == 6
}
#[inline]
fn is_nitrogen(an: u8) -> bool {
an == 7
}
#[inline]
fn is_oxygen(an: u8) -> bool {
an == 8
}
#[inline]
fn is_sulfur(an: u8) -> bool {
an == 16
}
#[inline]
fn is_halogen(an: u8) -> bool {
matches!(an, 9 | 17 | 35 | 53)
}
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() {
if atom.wildcard {
continue;
}
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() {
if atom.wildcard {
continue;
}
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();
(is_nitrogen(an) || is_oxygen(an) || an == 16) && implicit_hcount(mol, *idx) > 0
})
.count()
}
fn hba_count_from_set(mol: &Molecule, ring_bonds: &FxHashSet<BondIdx>) -> usize {
mol.atoms()
.filter(|(idx, atom)| {
let an = atom.element.atomic_number();
if is_nitrogen(an) {
if atom.charge != 0 {
return false;
}
let h = implicit_hcount(mol, *idx);
if atom.aromatic {
h == 0 && mol.degree(*idx) < 3
} else {
let bov = bond_order_sum(mol, *idx) as usize + h as usize;
if bov != 3 {
return false;
}
!n_adjacent_to_pi_center(mol, *idx, ring_bonds)
}
} else if is_oxygen(an) {
if atom.charge > 0 {
return false;
} if atom.charge < 0 {
return true;
} let impl_h = implicit_hcount(mol, *idx);
let expl_h = mol
.neighbors(*idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 1)
.count() as u8;
match impl_h + expl_h {
0 => bond_order_sum(mol, *idx) == 2,
1 => !neighbor_has_pi_bond_to_onps(mol, *idx),
_ => false,
}
} else if is_sulfur(an) {
if atom.charge < 0 {
return true;
} if atom.charge != 0 {
return false;
} if atom.aromatic {
true
} else {
let impl_h = implicit_hcount(mol, *idx);
let expl_h = mol
.neighbors(*idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 1)
.count() as u8;
let total_h = impl_h + expl_h;
let bos = bond_order_sum(mol, *idx) as usize + impl_h as usize;
if bos != 2 {
return false;
} match total_h {
1 => !neighbor_has_pi_bond_to_onps(mol, *idx),
0 => true,
_ => false,
}
}
} else {
false
}
})
.count()
}
pub fn hba_count(mol: &Molecule) -> usize {
hba_count_from_set(mol, &ring_bond_indices(mol))
}
fn neighbor_has_pi_bond_to_onps(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx).any(|(nb_idx, _)| {
has_double_bond_to(mol, nb_idx, 7) || has_double_bond_to(mol, nb_idx, 8) || has_double_bond_to(mol, nb_idx, 15) || has_double_bond_to(mol, nb_idx, 16) })
}
fn has_nonring_double_bond_to(
mol: &Molecule,
nb_idx: AtomIdx,
target_an: u8,
ring_bonds: &FxHashSet<BondIdx>,
) -> bool {
mol.neighbors(nb_idx).any(|(far, bidx)| {
mol.bond(bidx).order == BondOrder::Double
&& mol.atom(far).element.atomic_number() == target_an
&& !ring_bonds.contains(&bidx)
})
}
fn n_adjacent_to_pi_center(mol: &Molecule, idx: AtomIdx, ring_bonds: &FxHashSet<BondIdx>) -> bool {
mol.neighbors(idx).any(|(nb_idx, bidx)| {
matches!(
mol.bond(bidx).order,
BondOrder::Single | BondOrder::Up | BondOrder::Down | BondOrder::Aromatic
) && (has_nonring_double_bond_to(mol, nb_idx, 8, ring_bonds) || has_nonring_double_bond_to(mol, nb_idx, 7, ring_bonds) || has_nonring_double_bond_to(mol, nb_idx, 16, ring_bonds) || has_nonring_double_bond_to(mol, nb_idx, 15, ring_bonds)) })
}
fn rotatable_bond_count_from_set(mol: &Molecule, ring_bond_set: &FxHashSet<BondIdx>) -> usize {
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_carbonyl_hetero_bond(mol, bond.atom1, bond.atom2)
&& !is_diacyl_cc_bond(mol, bond.atom1, bond.atom2)
&& !is_neopentyl_like(mol, bond.atom1, bond.atom2)
&& !has_triple_bond(mol, bond.atom1)
&& !has_triple_bond(mol, bond.atom2)
&& !is_cumulated_double(mol, bond.atom1)
&& !is_cumulated_double(mol, bond.atom2)
})
.count()
}
pub fn rotatable_bond_count(mol: &Molecule) -> usize {
rotatable_bond_count_from_set(mol, &ring_bond_indices(mol))
}
fn has_triple_bond(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx)
.any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Triple)
}
fn is_cumulated_double(mol: &Molecule, idx: AtomIdx) -> bool {
mol.atom(idx).element.atomic_number() == 6
&& mol
.neighbors(idx)
.filter(|(_, bidx)| mol.bond(*bidx).order == BondOrder::Double)
.count()
>= 2
}
fn ring_bond_indices_from_rings(mol: &Molecule, rings: &[Vec<AtomIdx>]) -> FxHashSet<BondIdx> {
let mut set = FxHashSet::default();
for ring in 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 ring_bond_indices(mol: &Molecule) -> FxHashSet<BondIdx> {
ring_bond_indices_from_rings(mol, find_sssr(mol).rings())
}
fn is_neopentyl_like(mol: &Molecule, a: AtomIdx, b: AtomIdx) -> bool {
is_neopentyl_center(mol, a, b) || is_neopentyl_center(mol, b, a)
}
fn is_neopentyl_center(mol: &Molecule, center: AtomIdx, other: AtomIdx) -> bool {
if mol.atom(center).element.atomic_number() != 6 {
return false;
}
if mol.degree(center) != 4 {
return false;
}
let others: Vec<AtomIdx> = mol
.neighbors(center)
.filter(|(nb, _)| *nb != other)
.map(|(nb, _)| nb)
.collect();
if !others.iter().all(|&nb| mol.degree(nb) == 1) {
return false;
}
let an0 = mol.atom(others[0]).element.atomic_number();
others
.iter()
.all(|&nb| mol.atom(nb).element.atomic_number() == an0)
}
fn is_diacyl_cc_bond(mol: &Molecule, a: AtomIdx, b: AtomIdx) -> bool {
if mol.atom(a).element.atomic_number() != 6 || mol.atom(b).element.atomic_number() != 6 {
return false;
}
let is_acyl = |idx: AtomIdx| {
has_double_bond_to(mol, idx, 8)
&& mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Single
&& matches!(mol.atom(nb).element.atomic_number(), 7 | 8 | 16)
})
};
is_acyl(a) && is_acyl(b)
}
fn is_carbonyl_hetero_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();
if an_a == 7 && an_b == 7 {
let adj_carbonyl = |n: AtomIdx| {
mol.neighbors(n).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 6 && has_double_bond_to(mol, nb, 8)
})
};
return adj_carbonyl(a) && adj_carbonyl(b);
}
let c_idx = match (an_a, an_b) {
(6, 7) | (6, 8) | (6, 16) => a, (7, 6) | (8, 6) | (16, 6) => b, _ => return false,
};
if mol.degree(c_idx) <= 2 {
return false;
}
has_double_bond_to(mol, c_idx, 8) || has_double_bond_to(mol, c_idx, 7) || has_double_bond_to(mol, c_idx, 16) }
fn tpsa_nitrogen(
mol: &Molecule,
idx: AtomIdx,
is_aromatic: bool,
h: u8,
charge: i8,
ring_bonds: &FxHashSet<BondIdx>,
) -> f64 {
if is_aromatic {
let degree = mol.degree(idx);
if h > 0 {
15.79
} else if degree >= 3 {
let is_ring_junction = mol
.neighbors(idx)
.all(|(_, bidx)| mol.bond(bidx).order == BondOrder::Aromatic);
if charge > 0 {
if is_ring_junction { 4.10 } else { 3.88 }
} else {
if is_ring_junction { 4.41 } else { 4.93 }
}
} else {
12.89
}
} else {
if 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 {
43.14 } else if has_double_bond_to(mol, idx, 7) {
14.10 } else if has_double_bond_to(mol, idx, 6) {
3.01 } else {
0.00
}
} else if h >= 2 {
26.02
} else if h == 1 {
if has_double_bond_to(mol, idx, 6) {
23.85
} else {
12.03
}
} else if charge < 0 {
if has_double_bond_to(mol, idx, 7) {
22.30
} else {
12.36
}
} else {
let has_triple_to_c = mol.neighbors(idx).any(|(nb, bidx)| {
mol.bond(bidx).order == BondOrder::Triple
&& mol.atom(nb).element.atomic_number() == 6
});
if has_triple_to_c {
23.79 } else if has_double_bond_to(mol, idx, 6)
|| has_double_bond_to(mol, idx, 8)
|| has_double_bond_to(mol, idx, 7)
|| has_double_bond_to(mol, idx, 15)
|| has_double_bond_to(mol, idx, 16)
{
12.36 } else {
let ring_nbs: Vec<_> = mol
.neighbors(idx)
.filter(|(_, b)| ring_bonds.contains(b))
.map(|(nb, _)| nb)
.collect();
let in_3ring =
ring_nbs.len() == 2 && mol.bond_between(ring_nbs[0], ring_nbs[1]).is_some();
let has_external_ps = mol.neighbors(idx).any(|(nb, bidx)| {
let an = mol.atom(nb).element.atomic_number();
(an == 15 || an == 16) && !ring_bonds.contains(&bidx)
});
if in_3ring && has_external_ps {
3.01
} else {
3.24
}
}
}
}
}
fn tpsa_oxygen(mol: &Molecule, idx: AtomIdx, is_aromatic: bool, h: u8, charge: i8) -> f64 {
if is_aromatic {
13.14
} else if h > 0 {
if mol.neighbors(idx).count() == 0 {
31.50
} else {
20.23
}
} else {
if charge == -1 {
let is_nitro_o_minus = mol.neighbors(idx).any(|(nb, _)| {
let n = mol.atom(nb);
n.element.atomic_number() == 7
&& n.charge == 1
&& mol.neighbors(nb).any(|(nb2, bidx2)| {
mol.atom(nb2).element.atomic_number() == 8
&& mol.bond(bidx2).order == BondOrder::Double
})
});
return if is_nitro_o_minus { 0.0 } else { 23.06 };
}
let dbl_nb_pair = mol
.neighbors(idx)
.find(|&(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
let dbl_nb = dbl_nb_pair
.map(|(nei, _)| (mol.atom(nei).element.atomic_number(), mol.atom(nei).charge));
match dbl_nb {
Some((6, _)) => 17.07,
Some((7, 0)) => 17.07,
Some((7, _)) => 0.0,
Some((34, _)) => 17.07,
Some((16, _)) => {
let s_idx = dbl_nb_pair.unwrap().0;
if has_double_bond_to(mol, s_idx, 7) {
17.07
} else {
0.0
}
}
Some(_) => 0.0,
None => {
if is_aromatic_oxide_bridge(mol, idx) {
13.14
} else {
let nbs: Vec<AtomIdx> = mol.neighbors(idx).map(|(nb, _)| nb).collect();
if nbs.len() == 2 && mol.bond_between(nbs[0], nbs[1]).is_some() {
12.53
} else {
9.23
}
}
}
}
}
}
fn tpsa_sulfur(mol: &Molecule, idx: AtomIdx, is_aromatic: bool, h: u8, charge: i8) -> f64 {
if charge > 0 {
return 0.0;
}
if is_aromatic {
28.24
} else if h > 0 {
38.80
} else {
match count_double_bonds_to(mol, idx, 8) {
0 => {
if has_double_bond_to(mol, idx, 6) {
32.09 } else if has_double_bond_to(mol, idx, 7) && is_atom_in_ring(mol, idx) {
19.21 } else {
25.30 }
}
1 => {
if has_double_bond_to(mol, idx, 7) {
8.38
} else {
36.28
}
}
_ => 42.52,
}
}
}
fn tpsa_phosphorus(mol: &Molecule, idx: AtomIdx, h: u8) -> f64 {
if has_double_bond_to(mol, idx, 8) {
26.88 } else if has_double_bond_to(mol, idx, 7) {
9.81 } else if h > 0 {
34.14 } else {
13.59 }
}
pub fn tpsa(mol: &Molecule) -> f64 {
let mol_arom = chematic_perception::apply_aromaticity(mol);
let ring_bonds = ring_bond_indices(&mol_arom);
let mut psa = 0.0f64;
for (idx, atom_arom) in mol_arom.atoms() {
let orig_atom = mol.atom(idx);
let an = orig_atom.element.atomic_number();
let is_aromatic = if an == 7 {
orig_atom.aromatic
} else {
atom_arom.aromatic
};
let h = if an == 7 {
implicit_hcount(mol, idx)
} else {
implicit_hcount(&mol_arom, idx)
};
let contribution = match an {
7 => tpsa_nitrogen(mol, idx, is_aromatic, h, orig_atom.charge, &ring_bonds),
8 => tpsa_oxygen(&mol_arom, idx, is_aromatic, h, atom_arom.charge),
16 => tpsa_sulfur(&mol_arom, idx, is_aromatic, h, atom_arom.charge),
15 if !is_aromatic => tpsa_phosphorus(&mol_arom, idx, h),
_ => 0.0,
};
psa += contribution;
}
psa
}
static CRIPPEN_SMARTS: &[(&str, f64, f64)] = &[
("[CH4]", 0.1441, 2.503),
("[CH3]C", 0.1441, 2.503),
("[CH2](C)C", 0.1441, 2.503),
("[CH](C)(C)C", 0.0000, 2.433),
("[C](C)(C)(C)C", 0.0000, 2.433),
("[CH3][N,O,P,S,F,Cl,Br,I]", -0.2035, 2.753),
("[CH2X4]([N,O,P,S,F,Cl,Br,I])[A;!#1]", -0.2035, 2.753),
(
"[CH1X4]([N,O,P,S,F,Cl,Br,I])([A;!#1])[A;!#1]",
-0.2051,
2.731,
),
(
"[CH0X4]([N,O,P,S,F,Cl,Br,I])([A;!#1])([A;!#1])[A;!#1]",
-0.2051,
2.731,
),
("[C]=[!C;A;!#1]", -0.2783, 5.007),
("[CH2]=C", 0.1551, 3.513),
("[CH1](=C)[A;!#1]", 0.1551, 3.513),
("[CH0](=C)([A;!#1])[A;!#1]", 0.1551, 3.513),
("[C](=C)=C", 0.1551, 3.513),
("[CX2]#[A;!#1]", 0.0017, 3.888),
("[CH3]c", 0.08452, 2.464),
("[CH3]a", -0.1444, 2.412),
("[CH2X4]a", -0.0516, 2.488),
("[CHX4]a", 0.1193, 2.582),
("[CH0X4]a", -0.0967, 2.576),
("[cH0]-[A;!C;!N;!O;!S;!F;!Cl;!Br;!I;!#1]", -0.5443, 4.041),
("[c][#9]", 0.0000, 3.257),
("[c][#17]", 0.2450, 3.564),
("[c][#35]", 0.1980, 3.180),
("[c][#53]", 0.0000, 3.104),
("[cH]", 0.1581, 3.350),
("[c](:a)(:a):a", 0.2955, 4.346),
("[c](:a)(:a)-a", 0.2713, 3.904),
("[c](:a)(:a)-C", 0.1360, 3.509),
("[c](:a)(:a)-N", 0.4619, 4.067),
("[c](:a)(:a)-O", 0.5437, 3.853),
("[c](:a)(:a)-S", 0.1893, 2.673),
("[c](:a)(:a)=[C,N,O]", -0.8186, 3.135),
("[C](=C)(a)[A;!#1]", 0.2640, 4.305),
("[C](=C)(c)a", 0.2640, 4.305),
("[CH1](=C)a", 0.2640, 4.305),
("[C]=c", 0.2640, 4.305),
("[CX4][A;!C;!N;!O;!P;!S;!F;!Cl;!Br;!I;!#1]", 0.2148, 2.693),
("[#6]", 0.08129, 3.243), ("[#1][#6,#1]", 0.1230, 1.057),
("[#1]O[CX4,c]", -0.2677, 1.395),
("[#1]O[!C;!N;!O;!S]", -0.2677, 1.395),
("[#1][!C;!N;!O]", -0.2677, 1.395),
("[#1][#7]", 0.2142, 0.9627),
("[#1]O[#7]", 0.2142, 0.9627),
("[#1]OC=[#6,#7,O,S]", 0.2980, 1.805),
("[#1]O[O,S]", 0.2980, 1.805),
("[#1]", 0.1125, 1.112), ("[NH2+0][A;!#1]", -1.0190, 2.262),
("[NH+0]([A;!#1])[A;!#1]", -0.7096, 2.173),
("[NH2+0]a", -1.0270, 2.827),
("[NH1+0]([!#1;A,a])a", -0.5188, 3.000),
("[NH+0]=[!#1;A,a]", 0.08387, 1.757),
("[N+0](=[!#1;A,a])[!#1;A,a]", 0.1836, 2.428),
("[N+0]([A;!#1])([A;!#1])[A;!#1]", -0.3187, 1.839),
("[N+0](a)([!#1;A,a])[A;!#1]", -0.4458, 2.819),
("[N+0](a)(a)a", -0.4458, 2.819),
("[N+0]#[A;!#1]", 0.01508, 1.725),
("[NH3,NH2,NH;+,+2,+3]", -1.9500, 0.000),
("[n+0]", -0.3239, 2.202),
("[n;+,+2,+3]", -1.1190, 0.000),
(
"[NH0;+,+2,+3]([A;!#1])([A;!#1])([A;!#1])[A;!#1]",
-0.3396,
0.2604,
),
("[NH0;+,+2,+3](=[A;!#1])([A;!#1])[!#1;A,a]", -0.3396, 0.2604),
("[NH0;+,+2,+3](=[#6])=[#7]", -0.3396, 0.2604),
("[N;+,+2,+3]#[A;!#1]", 0.2887, 3.359),
("[N;-,-2,-3]", 0.2887, 3.359),
("[N;+,+2,+3](=[N;-,-2,-3])=N", 0.2887, 3.359),
("[#7]", -0.4806, 2.134), ("[o]", 0.1552, 1.080),
("[OH,OH2]", -0.2893, 0.8238),
("[O]([A;!#1])[A;!#1]", -0.0684, 1.085),
("[O](a)[!#1;A,a]", -0.4195, 1.182),
("[O]=[#7,#8]", 0.0335, 3.367),
("[OX1;-,-2,-3][#7]", 0.0335, 3.367),
("[OX1;-,-2,-3][#16]", -0.3339, 0.7774),
("[O;-0]=[#16;-0]", -0.3339, 0.7774),
("[O-]C(=O)", -1.3260, 0.000),
("[OX1;-,-2,-3][!#1;!N;!S]", -1.1890, 0.000),
("[O]=c", 0.1788, 3.135),
("[O]=[CH]C", -0.1526, 0.000),
("[O]=C(C)([A;!#1])", -0.1526, 0.000),
("[O]=[CH][N,O]", -0.1526, 0.000),
("[O]=[CH2]", -0.1526, 0.000),
("[O]=[CX2]=O", -0.1526, 0.000),
("[O]=[CH]c", 0.1129, 0.2215),
("[O]=C([C,c])[a;!#1]", 0.1129, 0.2215),
("[O]=C(c)[A;!#1]", 0.1129, 0.2215),
("[O]=C([!#1;!#6])[!#1;!#6]", 0.4833, 0.3890),
("[#8]", -0.1188, 0.6865), ("[S;-,-2,-3,-4,+1,+2,+3,+5,+6]", -0.0024, 7.365),
("[S;-0]=[N,O,P,S]", -0.0024, 7.365),
("[S;A]", 0.6482, 7.591),
("[s;a]", 0.6237, 6.691),
("[#15]", 0.8612, 6.920),
("[#9;-0]", 0.4202, 1.108),
("[#17;-0]", 0.6895, 5.853),
("[#35;-0]", 0.8456, 8.927),
("[#53;-0]", 0.8857, 14.020),
("[#9,#17,#35,#53;-]", -2.9960, 0.000),
("[#53;+,+2,+3]", -2.9960, 0.000),
("[+;#3,#11,#19,#37,#55]", -2.9960, 0.000),
("[#3,#11,#19,#37,#55]", -0.3808, 5.754),
("[#4,#12,#20,#38,#56]", -0.3808, 5.754),
("[#5,#13,#31,#49,#81]", -0.3808, 5.754),
("[#14,#32,#50,#82]", -0.3808, 5.754),
("[#33,#51,#83]", -0.3808, 5.754),
("[#34,#52,#84]", -0.3808, 5.754),
("[#21,#22,#23,#24,#25,#26,#27,#28,#29,#30]", -0.0025, 0.000),
("[#39,#40,#41,#42,#43,#44,#45,#46,#47,#48]", -0.0025, 0.000),
("[#72,#73,#74,#75,#76,#77,#78,#79,#80]", -0.0025, 0.000),
];
type CrippenQueries = Vec<(Option<chematic_smarts::QueryMolecule>, f64, f64)>;
static CRIPPEN_QUERIES: OnceLock<CrippenQueries> = OnceLock::new();
fn get_crippen_queries() -> &'static CrippenQueries {
CRIPPEN_QUERIES.get_or_init(|| {
CRIPPEN_SMARTS
.iter()
.map(|&(sma, lp, mr)| (parse_smarts(sma).ok(), lp, mr))
.collect()
})
}
fn crippen_anchor_sets(mol: &Molecule, queries: &CrippenQueries) -> Vec<FxHashSet<AtomIdx>> {
let rings = find_sssr(mol);
let config = MatchConfig {
uniquify: false,
..MatchConfig::default()
};
queries
.iter()
.map(|(q_opt, _, _)| {
let Some(q) = q_opt else {
return FxHashSet::default();
};
find_matches_with_rings_and_config(q, mol, &rings, &config)
.into_iter()
.filter_map(|m| m.get(&0).copied())
.collect()
})
.collect()
}
pub fn logp_crippen_per_atom(mol: &Molecule) -> Vec<f64> {
let queries = get_crippen_queries();
let anchor_sets = crippen_anchor_sets(mol, queries);
let h_fallback = CRIPPEN_SMARTS
.iter()
.find(|(sma, _, _)| *sma == "[#1]")
.map(|e| e.1)
.unwrap_or(0.1125);
mol.atoms()
.map(|(idx, atom)| {
if atom.element.atomic_number() == 1 {
return 0.0;
}
let h_count_impl = implicit_hcount(mol, idx);
let h_count_expl = mol
.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 1)
.count() as u8;
let h_count = h_count_impl + h_count_expl;
let heavy = if atom.element.atomic_number() == 8
&& !atom.aromatic
&& h_count == 0
&& atom.charge == 0
&& is_aromatic_oxide_bridge(mol, idx)
{
0.1552
} else {
anchor_sets
.iter()
.zip(queries.iter())
.find(|(set, _)| set.contains(&idx))
.map(|(_, (_, logp, _))| *logp)
.unwrap_or(0.0)
};
let h_contrib = if h_count == 0 {
0.0
} else {
h_logp_for_parent(
mol,
idx,
atom.element.atomic_number(),
atom.aromatic,
h_fallback,
) * h_count as f64
};
heavy + h_contrib
})
.collect()
}
fn h_logp_for_parent(
mol: &Molecule,
parent_idx: AtomIdx,
parent_an: u8,
parent_aromatic: bool,
fallback: f64,
) -> f64 {
match parent_an {
6 => 0.1230, 7 => 0.2142, 8 => {
if parent_aromatic {
fallback } else if mol.neighbors(parent_idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 6
&& mol.neighbors(nb).any(|(nb2, bidx2)| {
nb2 != parent_idx
&& mol.bond(bidx2).order == BondOrder::Double
&& matches!(mol.atom(nb2).element.atomic_number(), 6 | 7 | 8 | 16)
})
}) {
0.2980
} else if mol.neighbors(parent_idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 7 }) {
0.2142
} else if mol.neighbors(parent_idx).any(|(nb, _)| {
let an = mol.atom(nb).element.atomic_number();
an == 8 || an == 16 }) {
0.2980
} else {
-0.2677
}
}
_ => -0.2677,
}
}
pub fn logp_crippen(mol: &Molecule) -> f64 {
logp_crippen_per_atom(mol).iter().sum()
}
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 {
use chematic_perception::count_aromatic_rings;
count_aromatic_rings(mol)
}
pub fn formal_charge_sum(mol: &Molecule) -> i32 {
mol.atoms().map(|(_, a)| a.charge as i32).sum()
}
fn h_mr_for_parent(
mol: &Molecule,
parent_idx: AtomIdx,
parent_an: u8,
parent_aromatic: bool,
fallback: f64,
) -> f64 {
match parent_an {
6 => 1.057, 7 => 0.9627, 8 => {
if parent_aromatic {
fallback } else if mol.neighbors(parent_idx).any(|(nb, _)| {
mol.atom(nb).element.atomic_number() == 7
}) {
0.9627
} else if mol.neighbors(parent_idx).any(|(nb, _)| {
let an = mol.atom(nb).element.atomic_number();
(an == 6
&& mol.neighbors(nb).any(|(nb2, bidx2)| {
nb2 != parent_idx
&& mol.bond(bidx2).order == BondOrder::Double
&& matches!(mol.atom(nb2).element.atomic_number(), 6 | 7 | 8 | 16)
}))
|| an == 8
|| an == 16
}) {
1.805
} else {
1.395 }
}
_ => 1.395, }
}
pub fn mr_per_atom(mol: &Molecule) -> Vec<f64> {
let queries = get_crippen_queries();
let anchor_sets = crippen_anchor_sets(mol, queries);
let h_fallback = CRIPPEN_SMARTS
.iter()
.find(|(sma, _, _)| *sma == "[#1]")
.map(|e| e.2)
.unwrap_or(1.112);
mol.atoms()
.map(|(idx, atom)| {
if atom.element.atomic_number() == 1 {
return 0.0;
}
let h_count_impl = implicit_hcount(mol, idx);
let h_count_expl = mol
.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 1)
.count() as u8;
let h_count = h_count_impl + h_count_expl;
let heavy = if atom.element.atomic_number() == 8
&& !atom.aromatic
&& h_count == 0
&& atom.charge == 0
&& is_aromatic_oxide_bridge(mol, idx)
{
1.080 } else {
anchor_sets
.iter()
.zip(queries.iter())
.find(|(set, _)| set.contains(&idx))
.map(|(_, (_, _, mr))| *mr)
.unwrap_or(0.0)
};
let h_contrib = if h_count == 0 {
0.0
} else {
h_mr_for_parent(
mol,
idx,
atom.element.atomic_number(),
atom.aromatic,
h_fallback,
) * h_count as f64
};
heavy + h_contrib
})
.collect()
}
pub fn molar_refractivity(mol: &Molecule) -> f64 {
mr_per_atom(mol).iter().sum()
}
pub fn logp_and_mr(mol: &Molecule) -> (f64, f64) {
let queries = get_crippen_queries();
let anchor_sets = crippen_anchor_sets(mol, queries);
let h_logp_fallback = CRIPPEN_SMARTS
.iter()
.find(|(sma, _, _)| *sma == "[#1]")
.map(|e| e.1)
.unwrap_or(0.1125);
let h_mr_fallback = CRIPPEN_SMARTS
.iter()
.find(|(sma, _, _)| *sma == "[#1]")
.map(|e| e.2)
.unwrap_or(1.112);
let mut logp_sum = 0.0f64;
let mut mr_sum = 0.0f64;
for (idx, atom) in mol.atoms() {
if atom.element.atomic_number() == 1 {
continue;
}
let h_count = implicit_hcount(mol, idx);
let logp_heavy = if atom.element.atomic_number() == 8
&& !atom.aromatic
&& h_count == 0
&& atom.charge == 0
&& is_aromatic_oxide_bridge(mol, idx)
{
0.1552
} else {
anchor_sets
.iter()
.zip(queries.iter())
.find(|(set, _)| set.contains(&idx))
.map(|(_, (_, lp, _))| *lp)
.unwrap_or(0.0)
};
let mr_heavy = anchor_sets
.iter()
.zip(queries.iter())
.find(|(set, _)| set.contains(&idx))
.map(|(_, (_, _, mr))| *mr)
.unwrap_or(0.0);
logp_sum += logp_heavy;
mr_sum += mr_heavy;
if h_count > 0 {
logp_sum += h_logp_for_parent(
mol,
idx,
atom.element.atomic_number(),
atom.aromatic,
h_logp_fallback,
) * h_count as f64;
mr_sum += h_mr_for_parent(
mol,
idx,
atom.element.atomic_number(),
atom.aromatic,
h_mr_fallback,
) * h_count as f64;
}
}
(logp_sum, mr_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 ring_system_count(mol: &Molecule) -> usize {
use chematic_perception::find_ring_families;
let sssr = find_sssr(mol);
find_ring_families(mol, &sssr).len()
}
pub fn hba_count_lipinski(mol: &Molecule) -> usize {
mol.atoms()
.filter(|(_, a)| {
let an = a.element.atomic_number();
an == 7 || an == 8
})
.count()
}
pub fn fraction_rotatable_bonds(mol: &Molecule) -> f64 {
let hac = heavy_atom_count(mol);
if hac == 0 {
return 0.0;
}
rotatable_bond_count(mol) as f64 / hac as f64
}
fn is_aliphatic_ring(mol: &Molecule, ring: &[AtomIdx]) -> bool {
ring.iter().any(|&idx| !mol.atom(idx).aromatic) || !ring_bonds_all_aromatic(mol, ring)
}
pub fn num_aliphatic_rings(mol: &Molecule) -> usize {
all_ring_list(mol)
.iter()
.filter(|ring| is_aliphatic_ring(mol, ring))
.count()
}
pub fn num_saturated_rings(mol: &Molecule) -> usize {
all_ring_list(mol)
.iter()
.filter(|ring| is_ring_saturated(mol, ring))
.count()
}
fn is_ring_saturated(mol: &Molecule, ring: &[AtomIdx]) -> bool {
let n = ring.len();
(0..n).all(|i| {
let a = ring[i];
let b = ring[(i + 1) % n];
mol.bond_between(a, b)
.map(|(bidx, _)| {
!matches!(
mol.bond(bidx).order,
BondOrder::Double | BondOrder::Triple | BondOrder::Aromatic
)
})
.unwrap_or(true)
})
}
pub fn num_aromatic_heterocycles(mol: &Molecule) -> usize {
aromatic_ring_list(mol)
.iter()
.filter(|ring| {
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 {
all_ring_list(mol)
.iter()
.filter(|ring| {
is_aliphatic_ring(mol, ring)
&& 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 {
all_ring_list(mol)
.iter()
.filter(|ring| {
is_ring_saturated(mol, ring)
&& ring.iter().any(|&idx| {
let an = mol.atom(idx).element.atomic_number();
an != 6 && an != 1
})
})
.count()
}
fn num_spiro_atoms_from(mol: &Molecule, rings: &[Vec<AtomIdx>]) -> usize {
mol.atoms()
.filter(|(idx, _)| {
let member: Vec<_> = rings.iter().filter(|r| r.contains(idx)).collect();
if member.len() < 2 {
return false;
}
(0..member.len()).any(|i| {
(i + 1..member.len())
.any(|j| member[i].iter().filter(|a| member[j].contains(a)).count() == 1)
})
})
.count()
}
pub fn num_spiro_atoms(mol: &Molecule) -> usize {
num_spiro_atoms_from(mol, &all_ring_list(mol))
}
fn num_bridgehead_atoms_from(mol: &Molecule, rings: &[Vec<AtomIdx>]) -> usize {
let bond_lists: Vec<Vec<BondIdx>> = rings
.iter()
.map(|r| {
let mut bonds: Vec<BondIdx> = (0..r.len())
.filter_map(|i| {
let a = r[i];
let b = r[(i + 1) % r.len()];
mol.bond_between(a, b).map(|(bidx, _)| bidx)
})
.collect();
bonds.sort_unstable_by_key(|b| b.0);
bonds
})
.collect();
let mut bridgehead_set: FxHashSet<AtomIdx> = FxHashSet::default();
for i in 0..rings.len() {
for j in (i + 1)..rings.len() {
let mut shared_bonds: Vec<BondIdx> = Vec::new();
let (mut ai, mut bi) = (0usize, 0usize);
while ai < bond_lists[i].len() && bi < bond_lists[j].len() {
match bond_lists[i][ai].0.cmp(&bond_lists[j][bi].0) {
std::cmp::Ordering::Equal => {
shared_bonds.push(bond_lists[i][ai]);
ai += 1;
bi += 1;
}
std::cmp::Ordering::Less => ai += 1,
std::cmp::Ordering::Greater => bi += 1,
}
}
if shared_bonds.len() < 2 {
continue; }
let ring_j_set: FxHashSet<AtomIdx> = rings[j].iter().copied().collect();
for &atom in rings[i].iter().filter(|a| ring_j_set.contains(a)) {
let incident = shared_bonds
.iter()
.filter(|&&b| {
let bond = mol.bond(b);
bond.atom1 == atom || bond.atom2 == atom
})
.count();
if incident == 1 {
bridgehead_set.insert(atom);
}
}
}
}
bridgehead_set.len()
}
pub fn num_bridgehead_atoms(mol: &Molecule) -> usize {
num_bridgehead_atoms_from(mol, &all_ring_list(mol))
}
#[derive(Debug, Clone)]
pub struct RingBundle {
pub ring_count: usize,
pub ring_system_count: usize,
pub aromatic_ring_count: usize,
pub num_aliphatic_rings: usize,
pub num_saturated_rings: usize,
pub num_aromatic_heterocycles: usize,
pub num_aliphatic_heterocycles: usize,
pub num_saturated_heterocycles: usize,
pub num_spiro_atoms: usize,
pub num_bridgehead_atoms: usize,
pub rotatable_bond_count: usize,
pub hba_count: usize,
pub fraction_rotatable_bonds: f64,
}
pub fn ring_bundle(mol: &Molecule) -> RingBundle {
let sssr = find_sssr(mol);
let rings = sssr.rings();
let all_rings = all_ring_list(mol);
let ring_bonds = ring_bond_indices_from_rings(mol, rings);
let aromatic_ring_count = aromatic_ring_list(mol).len();
let rotatable_bond_count = rotatable_bond_count_from_set(mol, &ring_bonds);
let hba_count = hba_count_from_set(mol, &ring_bonds);
let hac = heavy_atom_count(mol);
let fraction_rotatable_bonds = if hac == 0 {
0.0
} else {
rotatable_bond_count as f64 / hac as f64
};
RingBundle {
ring_count: rings.len(),
ring_system_count: find_ring_families(mol, &sssr).len(),
aromatic_ring_count,
num_aliphatic_rings: all_rings
.iter()
.filter(|r| r.iter().any(|&i| !mol.atom(i).aromatic))
.count(),
num_saturated_rings: all_rings
.iter()
.filter(|r| is_ring_saturated(mol, r))
.count(),
num_aromatic_heterocycles: all_rings
.iter()
.filter(|r| {
r.iter().all(|&i| mol.atom(i).aromatic)
&& r.iter().any(|&i| {
let an = mol.atom(i).element.atomic_number();
an != 6 && an != 1
})
})
.count(),
num_aliphatic_heterocycles: all_rings
.iter()
.filter(|r| {
r.iter().any(|&i| !mol.atom(i).aromatic)
&& r.iter().any(|&i| {
let an = mol.atom(i).element.atomic_number();
an != 6 && an != 1
})
})
.count(),
num_saturated_heterocycles: all_rings
.iter()
.filter(|r| {
is_ring_saturated(mol, r)
&& r.iter().any(|&i| {
let an = mol.atom(i).element.atomic_number();
an != 6 && an != 1
})
})
.count(),
num_spiro_atoms: num_spiro_atoms_from(mol, &all_rings),
num_bridgehead_atoms: num_bridgehead_atoms_from(mol, &all_rings),
rotatable_bond_count,
hba_count,
fraction_rotatable_bonds,
}
}
pub fn num_stereocenters(mol: &Molecule) -> usize {
use chematic_core::CipCode;
use std::collections::HashMap;
let provisional: HashMap<_, CipCode> = crate::cip::assign_cip(mol)
.assignments
.into_iter()
.filter(|(_, c)| matches!(c, CipCode::R | CipCode::S))
.collect();
mol.atoms()
.filter(|(idx, _)| crate::cip::is_potential_stereocenter_rule5(mol, *idx, &provisional))
.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.degree(*idx);
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 mcf_passes(mol: &Molecule) -> bool {
crate::pains_passes(mol)
&& crate::brenk_passes(mol)
&& lipinski_passes(mol)
&& veber_passes(mol)
}
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)
}
pub fn ro3_passes(mol: &Molecule) -> bool {
molecular_weight(mol) <= 300.0
&& logp_crippen(mol) <= 3.0
&& hbd_count(mol) <= 3
&& hba_count(mol) <= 3
&& rotatable_bond_count(mol) <= 3
}
pub fn lead_like_passes(mol: &Molecule) -> bool {
let mw = molecular_weight(mol);
let lp = logp_crippen(mol);
let rotb = rotatable_bond_count(mol);
let rings = ring_count(mol);
mw <= 450.0 && (-3.5..=4.5).contains(&lp) && rotb <= 10 && (1..=4).contains(&rings)
}
pub fn pfizer_3_75_passes(mol: &Molecule) -> bool {
!(logp_crippen(mol) > 3.0 && tpsa(mol) < 75.0)
}
pub fn cns_mpo_from_parts(
mol: &Molecule,
logp: f64,
tpsa: f64,
mw: f64,
hbd: usize,
pka_b: f64,
) -> f64 {
#[inline]
fn ld(val: f64, lo: f64, hi: f64) -> f64 {
if val <= lo {
1.0
} else if val >= hi {
0.0
} else {
(hi - val) / (hi - lo)
}
}
let d_tpsa = if !(0.0..=120.0).contains(&tpsa) {
0.0
} else if tpsa <= 40.0 {
tpsa / 40.0
} else if tpsa <= 90.0 {
1.0
} else {
(120.0 - tpsa) / 30.0
};
ld(logp, 3.0, 5.0)
+ ld(crate::logd::logd_from_logp(logp, mol, 7.4), 2.0, 4.0)
+ ld(mw, 360.0, 500.0)
+ d_tpsa
+ (1.0 - hbd as f64 / 2.0).clamp(0.0, 1.0)
+ ld(pka_b, 8.0, 10.0)
}
pub fn cns_mpo_score(mol: &Molecule) -> f64 {
#[inline]
fn linear_desirability(val: f64, lo: f64, hi: f64) -> f64 {
if val <= lo {
1.0
} else if val >= hi {
0.0
} else {
(hi - val) / (hi - lo)
}
}
let logp = logp_crippen(mol);
let d_logp = linear_desirability(logp, 3.0, 5.0);
let d_logd = linear_desirability(crate::logd::logd_from_logp(logp, mol, 7.4), 2.0, 4.0);
let d_mw = linear_desirability(molecular_weight(mol), 360.0, 500.0);
let psa = tpsa(mol);
let d_tpsa = if !(0.0..=120.0).contains(&psa) {
0.0
} else if psa <= 40.0 {
psa / 40.0
} else if psa <= 90.0 {
1.0
} else {
(120.0 - psa) / 30.0
};
let hbd = hbd_count(mol) as f64;
let d_hbd = (1.0 - hbd / 2.0).clamp(0.0, 1.0);
let pka_b = crate::pka::pka_base(mol).unwrap_or(0.0);
let d_pka = linear_desirability(pka_b, 8.0, 10.0);
d_logp + d_logd + d_mw + d_tpsa + d_hbd + d_pka
}
pub fn hybridization_per_atom(mol: &Molecule) -> Vec<u8> {
let n = mol.atom_count();
let mut out = vec![3u8; n];
for (idx, atom) in mol.atoms() {
let i = idx.0 as usize;
if atom.wildcard {
out[i] = 0;
continue;
}
if atom.aromatic {
out[i] = 2;
continue;
}
let mut has_triple = false;
let mut has_double = false;
for (_, bidx) in mol.neighbors(idx) {
match mol.bond(bidx).order {
BondOrder::Triple => has_triple = true,
BondOrder::Double => has_double = true,
_ => {}
}
}
out[i] = if has_triple {
1
} else if has_double {
2
} else {
3
};
}
out
}
pub fn formal_charge_per_atom(mol: &Molecule) -> Vec<i8> {
mol.atoms().map(|(_, a)| a.charge).collect()
}
pub fn implicit_hcount_per_atom(mol: &Molecule) -> Vec<u8> {
mol.atoms()
.map(|(idx, _)| implicit_hcount(mol, idx))
.collect()
}
pub fn tpsa_per_atom(mol: &Molecule) -> Vec<f64> {
let mol_arom = chematic_perception::apply_aromaticity(mol);
let mol = &mol_arom;
let ring_bonds = ring_bond_indices(mol);
let n = mol.atom_count();
let mut out = vec![0.0f64; n];
for (idx, atom) in mol.atoms() {
let an = atom.element.atomic_number();
let h = implicit_hcount(mol, idx);
out[idx.0 as usize] = match an {
7 => tpsa_nitrogen(mol, idx, atom.aromatic, h, atom.charge, &ring_bonds),
8 => tpsa_oxygen(mol, idx, atom.aromatic, h, atom.charge),
16 => tpsa_sulfur(mol, idx, atom.aromatic, h, atom.charge),
15 if !atom.aromatic => tpsa_phosphorus(mol, idx, h),
_ => 0.0,
};
}
out
}
fn fill_mqn_stats(mqn: &mut [u8], vals: &mut [u8], base: usize) {
if !vals.is_empty() {
vals.sort();
mqn[base] = vals[0];
mqn[base + 1] = vals[vals.len() - 1];
let avg = vals.iter().map(|&v| v as usize).sum::<usize>() / vals.len();
mqn[base + 2] = avg.min(255) as u8;
}
}
pub fn mqn(mol: &Molecule) -> Vec<u8> {
let mut m = vec![0u8; 42];
mqn_atom_counts(mol, &mut m);
mqn_bond_counts(mol, &mut m);
let ring_set = find_sssr(mol);
let rings = ring_set.rings();
let ring_sets: Vec<FxHashSet<AtomIdx>> =
rings.iter().map(|r| r.iter().copied().collect()).collect();
mqn_ring_stats(mol, rings, &mut m);
mqn_degree_stats(mol, &mut m);
mqn_valence_stats(mol, &mut m);
mqn_h_counts(mol, &mut m);
mqn_charge_stats(mol, &mut m);
mqn_heteroatom_stats(mol, &mut m);
m[31] = rotatable_bond_count(mol).min(255) as u8;
m[32] = mol.atoms().filter(|(_, a)| a.aromatic).count().min(255) as u8;
m[33] = hbd_count(mol).min(255) as u8;
m[34] = hba_count(mol).min(255) as u8;
m[37] = heavy_atom_count(mol).min(255) as u8;
mqn_topology_stats(mol, rings, &ring_sets, &mut m);
m
}
fn mqn_atom_counts(mol: &Molecule, m: &mut [u8]) {
for (_, atom) in mol.atoms() {
let slot = match atom.element.atomic_number() {
6 => 0,
7 => 1,
8 => 2,
9 => 3,
14 => 4,
15 => 5,
16 => 6,
17 => 7,
35 => 8,
53 => 9,
_ => continue,
};
m[slot] = m[slot].saturating_add(1);
}
}
fn mqn_bond_counts(mol: &Molecule, m: &mut [u8]) {
let mut single = 0u8;
let mut double = 0u8;
let mut triple = 0u8;
let mut aromatic = 0u8;
for (_, bond) in mol.bonds() {
match bond.order {
BondOrder::Single => single = single.saturating_add(1),
BondOrder::Double => double = double.saturating_add(1),
BondOrder::Triple => triple = triple.saturating_add(1),
BondOrder::Aromatic => aromatic = aromatic.saturating_add(1),
_ => single = single.saturating_add(1),
}
}
m[10] = single;
m[11] = double;
m[12] = triple;
m[13] = aromatic;
}
fn ring_is_saturated(mol: &Molecule, ring: &[AtomIdx]) -> bool {
ring.iter().all(|&idx| {
mol.neighbors(idx)
.all(|(_, bidx)| !matches!(mol.bond(bidx).order, BondOrder::Double | BondOrder::Triple))
})
}
fn ring_has_heteroatom(mol: &Molecule, ring: &[AtomIdx]) -> bool {
ring.iter()
.any(|&idx| matches!(mol.atom(idx).element.atomic_number(), 7 | 8 | 16))
}
fn mqn_ring_stats(mol: &Molecule, rings: &[Vec<AtomIdx>], m: &mut [u8]) {
m[14] = rings.len().min(255) as u8;
let mut aromatic_rings = 0u8;
let mut saturated_rings = 0u8;
for ring in rings {
let is_aromatic = ring.iter().all(|&idx| mol.atom(idx).aromatic);
if is_aromatic {
aromatic_rings = (aromatic_rings as usize + 1).min(255) as u8;
} else if ring_is_saturated(mol, ring) {
saturated_rings = (saturated_rings as usize + 1).min(255) as u8;
}
if ring_has_heteroatom(mol, ring) {
if is_aromatic {
m[35] = m[35].saturating_add(1);
} else {
m[36] = m[36].saturating_add(1);
}
}
}
m[15] = aromatic_rings;
m[16] = saturated_rings;
}
fn mqn_degree_stats(mol: &Molecule, m: &mut [u8]) {
let mut degrees: Vec<u8> = mol.atoms().map(|(idx, _)| mol.degree(idx) as u8).collect();
fill_mqn_stats(m, &mut degrees, 17);
}
fn mqn_valence_stats(mol: &Molecule, m: &mut [u8]) {
let mut valences: Vec<u8> = mol
.atoms()
.map(|(idx, _)| (mol.degree(idx) + implicit_hcount(mol, idx) as usize) as u8)
.collect();
fill_mqn_stats(m, &mut valences, 20);
}
fn mqn_h_counts(mol: &Molecule, m: &mut [u8]) {
for (idx, atom) in mol.atoms() {
let h = implicit_hcount(mol, idx) as usize;
let slot = match atom.element.atomic_number() {
6 => 23,
7 => 24,
8 => 25,
_ => continue,
};
m[slot] = (m[slot] as usize + h).min(255) as u8;
}
}
fn mqn_charge_stats(mol: &Molecule, m: &mut [u8]) {
let charge_sum = formal_charge_sum(mol);
m[26] = (charge_sum.clamp(-127, 127) + 127) as u8;
m[27] = charge_sum.abs().min(255) as u8;
}
fn mqn_heteroatom_stats(mol: &Molecule, m: &mut [u8]) {
let mut hetero_degrees: Vec<u8> = mol
.atoms()
.filter(|(_, a)| {
let an = a.element.atomic_number();
is_nitrogen(an) || is_oxygen(an) || is_halogen(an)
})
.map(|(idx, _)| mol.degree(idx) as u8)
.collect();
fill_mqn_stats(m, &mut hetero_degrees, 28);
}
fn mqn_topology_stats(
mol: &Molecule,
rings: &[Vec<AtomIdx>],
ring_sets: &[FxHashSet<AtomIdx>],
m: &mut [u8],
) {
m[38] = mol
.atoms()
.filter(|(idx, a)| {
a.element.atomic_number() == 6
&& mol.degree(*idx) + implicit_hcount(mol, *idx) as usize == 4
})
.count()
.min(255) as u8;
let mut fused = 0u8;
for i in 0..ring_sets.len() {
for j in (i + 1)..ring_sets.len() {
if ring_sets[i].intersection(&ring_sets[j]).count() > 1 {
fused = fused.saturating_add(1);
}
}
}
m[39] = fused;
m[40] = mol
.atoms()
.filter(|(idx, _)| ring_sets.iter().filter(|r| r.contains(idx)).count() >= 2)
.count()
.min(255) as u8;
let mut spiro = 0u8;
for (idx, _) in mol.atoms() {
if rings.iter().filter(|r| r.contains(&idx)).count() >= 2
&& mol
.neighbors(idx)
.all(|(nb, _)| rings.iter().any(|r| r.contains(&nb)))
{
spiro = spiro.saturating_add(1);
}
}
m[41] = spiro;
}
fn atomic_valence(mol: &Molecule, idx: AtomIdx) -> f64 {
let degree = mol.degree(idx) as f64;
let h_count = implicit_hcount(mol, idx) as f64;
degree + h_count
}
fn topo_dist_usize(mol: &Molecule) -> Vec<Vec<usize>> {
crate::topo_descriptors::topological_distance_matrix(mol)
.iter()
.map(|row| row.iter().map(|&d| d as usize).collect())
.collect()
}
pub fn autocorr_2d(mol: &Molecule) -> Vec<f64> {
if mol.atom_count() < 2 {
return vec![0.0; 7];
}
let dist = topo_dist_usize(mol);
let n = mol.atom_count();
let mut result = vec![0.0; 7];
for lag in 1..=7 {
let mut sum = 0.0;
for (i, row) in dist.iter().enumerate().take(n) {
for (j, &distance) in row.iter().enumerate().take(n).skip(i + 1) {
if distance == lag {
let val_i = atomic_valence(mol, AtomIdx(i as u32));
let val_j = atomic_valence(mol, AtomIdx(j as u32));
sum += val_i * val_j;
}
}
}
result[lag - 1] = sum;
}
result
}
pub fn moran_autocorr(mol: &Molecule) -> Vec<f64> {
if mol.atom_count() < 2 {
return vec![0.0; 7];
}
let dist = topo_dist_usize(mol);
let n = mol.atom_count();
let vals: Vec<f64> = (0..n)
.map(|i| atomic_valence(mol, AtomIdx(i as u32)))
.collect();
let mean = vals.iter().sum::<f64>() / n as f64;
let denom: f64 = vals.iter().map(|&v| (v - mean).powi(2)).sum();
if denom == 0.0 {
return vec![0.0; 7];
}
(1..=7usize)
.map(|lag| {
let mut numer = 0.0;
let mut w = 0usize;
for i in 0..n {
for j in (i + 1)..n {
if dist[i][j] == lag {
numer += (vals[i] - mean) * (vals[j] - mean);
w += 1;
}
}
}
if w == 0 {
0.0
} else {
(n as f64 / w as f64) * numer / denom
}
})
.collect()
}
pub fn geary_autocorr(mol: &Molecule) -> Vec<f64> {
if mol.atom_count() < 2 {
return vec![1.0; 7];
}
let dist = topo_dist_usize(mol);
let n = mol.atom_count();
let vals: Vec<f64> = (0..n)
.map(|i| atomic_valence(mol, AtomIdx(i as u32)))
.collect();
let mean = vals.iter().sum::<f64>() / n as f64;
let denom: f64 = vals.iter().map(|&v| (v - mean).powi(2)).sum();
if denom == 0.0 {
return vec![1.0; 7]; }
(1..=7usize)
.map(|lag| {
let mut numer = 0.0;
let mut w = 0usize;
for i in 0..n {
for j in (i + 1)..n {
if dist[i][j] == lag {
numer += (vals[i] - vals[j]).powi(2);
w += 1;
}
}
}
if w == 0 {
1.0
} else {
((n - 1) as f64 / (2.0 * w as f64)) * numer / denom
}
})
.collect()
}
pub fn balaban_j(mol: &Molecule) -> f64 {
let n = mol.atom_count();
if !(2..=1000).contains(&n) {
return 0.0;
}
let m = mol.bond_count() as f64;
let mu = m - n as f64 + 1.0;
if mu + 1.0 == 0.0 {
return 0.0;
}
let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
for (_, bond) in mol.bonds() {
let i = bond.atom1.0 as usize;
let j = bond.atom2.0 as usize;
let order = bond.order.order_value().map(|v| v as f64).unwrap_or(1.5);
let w = 1.0 / order;
adj[i].push((j, w));
adj[j].push((i, w));
}
let s: Vec<f64> = (0..n).map(|i| balaban_distance_sum(&adj, i, n)).collect();
let mut total = 0.0;
for (_, bond) in mol.bonds() {
let i = bond.atom1.0 as usize;
let j = bond.atom2.0 as usize;
if s[i] > 0.0 && s[j] > 0.0 {
total += 1.0 / (s[i] * s[j]).sqrt();
}
}
(m / (mu + 1.0)) * total
}
fn balaban_distance_sum(adj: &[Vec<(usize, f64)>], start: usize, n: usize) -> f64 {
let mut dist = vec![f64::INFINITY; n];
let mut visited = vec![false; n];
dist[start] = 0.0;
for _ in 0..n {
let mut u = usize::MAX;
let mut best = f64::INFINITY;
for v in 0..n {
if !visited[v] && dist[v] < best {
best = dist[v];
u = v;
}
}
if u == usize::MAX {
break;
}
visited[u] = true;
for &(v, w) in &adj[u] {
let nd = dist[u] + w;
if nd < dist[v] {
dist[v] = nd;
}
}
}
dist.iter().filter(|d| d.is_finite()).sum()
}
pub fn ipc(mol: &Molecule) -> f64 {
let n = mol.atom_count();
if n < 2 {
return 0.0;
}
let dist = topo_dist_usize(mol);
let mut result = 0.0;
for (i, row) in dist.iter().enumerate().take(n) {
for (j, &distance) in row.iter().enumerate().take(n).skip(i + 1) {
let d = distance as f64;
if d > 0.0 {
let deg_i = mol.degree(AtomIdx(i as u32)) as f64;
let deg_j = mol.degree(AtomIdx(j as u32)) as f64;
result += (deg_i * deg_j) / (d * d);
}
}
}
result
}
fn hall_kier_radius(atomic_number: u8, hyb: u8) -> f64 {
match (atomic_number, hyb) {
(6, 1) => 0.60, (6, 2) => 0.67, (6, _) => 0.77, (7, 1) => 0.55, (7, 2) => 0.62, (7, _) => 0.70, (8, 2) => 0.60, (8, _) => 0.66, (9, _) => 0.64, (14, _) => 1.11, (15, _) => 1.07, (16, _) => 1.04, (17, _) => 0.99, (35, _) => 1.14, (53, _) => 1.33, _ => 0.77, }
}
pub fn hall_kier_alpha(mol: &Molecule) -> f64 {
const R_C_SP3: f64 = 0.77;
let hyb = hybridization_per_atom(mol);
let mut alpha_sum = 0.0;
for (idx, atom) in mol.atoms() {
if atom.element.atomic_number() == 1 {
continue; }
let r_i = hall_kier_radius(atom.element.atomic_number(), hyb[idx.0 as usize]);
alpha_sum += r_i / R_C_SP3 - 1.0;
}
alpha_sum
}
pub fn usrcat(mol: &Molecule) -> [f64; 42] {
let mut result = [0.0; 42];
if mol.atom_count() == 0 {
return result;
}
let dist_matrix = topo_dist_usize(mol);
let n = mol.atom_count();
let mut centroid_dist = 0.0;
for (i, row) in dist_matrix.iter().enumerate().take(n) {
for &distance in row.iter().take(n).skip(i + 1) {
centroid_dist += distance as f64;
}
}
if n > 1 {
centroid_dist /= (n * (n - 1) / 2) as f64;
}
for (slot, value) in result.iter_mut().enumerate().take(36) {
let scale = 1.0 + (slot as f64 / 12.0);
*value = centroid_dist * scale;
}
for idx in 0..n {
let atom = mol.atom(AtomIdx(idx as u32));
let an = atom.element.atomic_number();
if (is_nitrogen(an) || is_oxygen(an)) && implicit_hcount(mol, AtomIdx(idx as u32)) > 0 {
result[36] += 1.0; }
if is_nitrogen(an) || is_oxygen(an) {
result[37] += 1.0; }
if atom.aromatic {
result[38] += 1.0; }
if is_carbon(an) {
let degree = mol.degree(AtomIdx(idx as u32));
if degree > 0 && !atom.aromatic {
result[39] += 1.0; }
}
if atom.charge < 0 {
result[40] += 1.0; }
if atom.charge > 0 {
result[41] += 1.0; }
}
result
}
pub fn mmff94_charges(mol: &Molecule) -> Vec<f64> {
crate::mmff94_bci::mmff94_charges_bci(mol)
}
fn count_element(mol: &Molecule, atomic_num: u8) -> usize {
mol.atoms()
.filter(|(_, a)| a.element.atomic_number() == atomic_num)
.count()
}
pub fn num_carbons(mol: &Molecule) -> usize {
count_element(mol, 6)
}
pub fn num_nitrogens(mol: &Molecule) -> usize {
count_element(mol, 7)
}
pub fn num_oxygens(mol: &Molecule) -> usize {
count_element(mol, 8)
}
pub fn num_fluorines(mol: &Molecule) -> usize {
count_element(mol, 9)
}
pub fn num_chlorines(mol: &Molecule) -> usize {
count_element(mol, 17)
}
pub fn num_bromines(mol: &Molecule) -> usize {
count_element(mol, 35)
}
pub fn num_iodines(mol: &Molecule) -> usize {
count_element(mol, 53)
}
pub fn num_sulfurs(mol: &Molecule) -> usize {
count_element(mol, 16)
}
pub fn num_phosphorus(mol: &Molecule) -> usize {
count_element(mol, 15)
}
pub fn num_hydrogens(mol: &Molecule) -> usize {
mol.atoms()
.map(|(idx, atom)| {
let explicit = atom.hydrogen_count.unwrap_or(0) as usize;
let implicit = implicit_hcount(mol, idx) as usize;
explicit + implicit
})
.sum()
}
pub fn num_amide_bonds(mol: &Molecule) -> usize {
mol.bonds()
.filter(|(_, bond)| {
let is_single = matches!(
bond.order,
BondOrder::Single | BondOrder::Up | BondOrder::Down
);
if !is_single {
return false;
}
let an_a = mol.atom(bond.atom1).element.atomic_number();
let an_b = mol.atom(bond.atom2).element.atomic_number();
let (c_idx, n_idx) = match (an_a, an_b) {
(6, 7) => (bond.atom1, bond.atom2),
(7, 6) => (bond.atom2, bond.atom1),
_ => return false,
};
!mol.atom(n_idx).aromatic && has_double_bond_to(mol, c_idx, 8)
})
.count()
}
pub fn num_ester_bonds(mol: &Molecule) -> usize {
let mut count = 0;
for (idx, atom) in mol.atoms() {
if atom.element.atomic_number() != 6 {
continue;
}
let has_carbonyl_o = mol.neighbors(idx).any(|(nb, bid)| {
mol.atom(nb).element.atomic_number() == 8 && mol.bond(bid).order == BondOrder::Double
});
if !has_carbonyl_o {
continue;
}
for (o_idx, bid) in mol.neighbors(idx) {
let is_oxygen = mol.atom(o_idx).element.atomic_number() == 8;
let is_single = matches!(
mol.bond(bid).order,
BondOrder::Single | BondOrder::Up | BondOrder::Down
);
if !is_oxygen || !is_single {
continue;
}
let o_bonded_to_carbon = mol
.neighbors(o_idx)
.any(|(nb, _)| nb != idx && mol.atom(nb).element.atomic_number() == 6);
if o_bonded_to_carbon {
count += 1;
}
}
}
count
}
pub fn calc_mol_formula(mol: &Molecule) -> String {
use std::collections::BTreeMap;
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for (_, atom) in mol.atoms() {
let symbol = atom.element.symbol().to_string();
*counts.entry(symbol).or_insert(0) += 1;
}
let total_h: usize = mol
.atoms()
.map(|(idx, _)| implicit_hcount(mol, idx) as usize)
.sum();
if total_h > 0 {
*counts.entry("H".to_string()).or_insert(0) += total_h;
}
let mut formula = String::new();
if let Some(&c_count) = counts.get("C") {
formula.push('C');
if c_count > 1 {
formula.push_str(&c_count.to_string());
}
}
if let Some(&h_count) = counts.get("H") {
formula.push('H');
if h_count > 1 {
formula.push_str(&h_count.to_string());
}
}
for (symbol, &count) in counts.iter() {
if symbol != "C" && symbol != "H" {
formula.push_str(symbol);
if count > 1 {
formula.push_str(&count.to_string());
}
}
}
if formula.is_empty() {
formula.push_str("H0"); }
formula
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CarbonTypes {
pub c1sp1: u32,
pub c2sp1: u32,
pub c1sp2: u32,
pub c2sp2: u32,
pub c3sp2: u32,
pub c1sp3: u32,
pub c2sp3: u32,
pub c3sp3: u32,
}
pub fn carbon_types(mol: &Molecule) -> CarbonTypes {
let hyb = hybridization_per_atom(mol);
let mut ct = CarbonTypes::default();
for (idx, atom) in mol.atoms() {
if atom.element.atomic_number() != 6 {
continue; }
let h = hyb[idx.0 as usize];
let deg: u32 = mol
.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() != 1)
.count() as u32;
match (h, deg) {
(1, 1) => ct.c1sp1 += 1,
(1, 2) => ct.c2sp1 += 1,
(2, 1) => ct.c1sp2 += 1,
(2, 2) => ct.c2sp2 += 1,
(2, 3) => ct.c3sp2 += 1,
(3, 1) => ct.c1sp3 += 1,
(3, 2) => ct.c2sp3 += 1,
(3, 3) => ct.c3sp3 += 1,
_ => {}
}
}
ct
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct InformationContent {
pub ic: f64,
pub tic: f64,
pub sic: f64,
pub bic: f64,
pub cic: f64,
}
pub fn information_content(mol: &Molecule) -> InformationContent {
let heavy: Vec<_> = mol
.atoms()
.filter(|(_, a)| a.element.atomic_number() != 1)
.collect();
let n = heavy.len();
if n < 2 {
return InformationContent::default();
}
let mut counts: FxHashMap<(u8, u32), usize> = FxHashMap::default();
for (idx, atom) in &heavy {
let deg = mol
.neighbors(*idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() != 1)
.count() as u32;
*counts
.entry((atom.element.atomic_number(), deg))
.or_insert(0) += 1;
}
let n_f = n as f64;
let ic: f64 = counts
.values()
.map(|&c| {
let p = c as f64 / n_f;
-p * p.log2()
})
.sum();
let log_n = n_f.log2();
InformationContent {
ic,
tic: n_f * ic,
sic: if log_n > 0.0 { ic / log_n } else { 0.0 },
bic: ic * (n_f - 1.0) / n_f,
cic: (log_n - ic).max(0.0),
}
}
pub fn mde_carbon(mol: &Molecule) -> [f64; 10] {
let heavy: Vec<_> = mol
.atoms()
.filter(|(_, a)| a.element.atomic_number() != 1)
.map(|(idx, _)| idx)
.collect();
let heavy_count = heavy.len();
if heavy_count == 0 {
return [0.0; 10];
}
let mut c_neighbors: Vec<Option<u8>> = vec![None; mol.atom_count()];
for &idx in &heavy {
if mol.atom(idx).element.atomic_number() == 6 {
let cn = mol
.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 6)
.count()
.min(4) as u8;
c_neighbors[idx.0 as usize] = Some(cn);
}
}
let dist = crate::topo_descriptors::topological_distance_matrix(mol);
let heavy_pos: FxHashMap<u32, usize> = heavy
.iter()
.enumerate()
.map(|(p, &idx)| (idx.0, p))
.collect();
let mut out = [0.0f64; 10];
let n = heavy.len();
for pi in 0..n {
let ai = heavy[pi];
let xi = match c_neighbors[ai.0 as usize] {
Some(v) if v >= 1 => v,
_ => continue,
};
for pj in (pi + 1)..n {
let aj = heavy[pj];
let xj = match c_neighbors[aj.0 as usize] {
Some(v) if v >= 1 => v,
_ => continue,
};
let d = dist[pi][pj];
if d == 0 || d == u32::MAX {
continue;
}
let contrib = 1.0 / (d as f64).sqrt();
let (lo, hi) = if xi <= xj { (xi, xj) } else { (xj, xi) };
let slot = match (lo, hi) {
(1, 1) => 0,
(1, 2) => 1,
(1, 3) => 2,
(1, 4) => 3,
(2, 2) => 4,
(2, 3) => 5,
(2, 4) => 6,
(3, 3) => 7,
(3, 4) => 8,
(4, 4) => 9,
_ => continue,
};
out[slot] += contrib;
}
}
let _ = heavy_pos; out
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Bcut2D {
pub chghi: f64,
pub chglo: f64,
pub logphi: f64,
pub logplo: f64,
pub mrhi: f64,
pub mrlo: f64,
pub mwhi: f64,
pub mwlo: f64,
}
pub fn bcut2d(mol: &Molecule) -> Bcut2D {
let heavy: Vec<AtomIdx> = mol
.atoms()
.filter(|(_, a)| a.element.atomic_number() != 1)
.map(|(idx, _)| idx)
.collect();
let n = heavy.len();
if n == 0 {
return Bcut2D::default();
}
let logp_v = logp_crippen_per_atom(mol);
let mr_v = mr_per_atom(mol);
let charge_v: Vec<f64> = mol.atoms().map(|(_, a)| a.charge as f64).collect();
let mw_v: Vec<f64> = mol.atoms().map(|(_, a)| a.element.atomic_mass()).collect();
let props: [Vec<f64>; 4] = [
heavy.iter().map(|&i| charge_v[i.0 as usize]).collect(),
heavy.iter().map(|&i| logp_v[i.0 as usize]).collect(),
heavy.iter().map(|&i| mr_v[i.0 as usize]).collect(),
heavy.iter().map(|&i| mw_v[i.0 as usize]).collect(),
];
let mut bonded = vec![vec![0.0f64; n]; n];
for (pi, &ai) in heavy.iter().enumerate() {
for (pj, &aj) in heavy.iter().enumerate().skip(pi + 1) {
if let Some((bidx, _)) = mol.bond_between(ai, aj) {
let bo = match mol.bond(bidx).order {
chematic_core::BondOrder::Single => 1.0,
chematic_core::BondOrder::Double => 2.0,
chematic_core::BondOrder::Triple => 3.0,
chematic_core::BondOrder::Aromatic => 1.5,
chematic_core::BondOrder::Up | chematic_core::BondOrder::Down => 1.0,
_ => 1.0,
};
let zi = mol.atom(ai).element.atomic_number() as f64;
let zj = mol.atom(aj).element.atomic_number() as f64;
let w = (zi * zj).sqrt() * bo / 8.0;
bonded[pi][pj] = w;
bonded[pj][pi] = w;
}
}
}
let compute = |prop: &[f64]| -> (f64, f64) {
let mut mat = vec![vec![0.001f64; n]; n];
for i in 0..n {
mat[i][i] = prop[i];
for j in (i + 1)..n {
if bonded[i][j] > 0.0 {
mat[i][j] = bonded[i][j];
mat[j][i] = bonded[i][j];
}
}
}
let eigs = jacobi_eigenvalues(&mat);
let hi = eigs.iter().copied().fold(f64::MIN, f64::max);
let lo = eigs.iter().copied().fold(f64::MAX, f64::min);
(hi, lo)
};
let (chghi, chglo) = compute(&props[0]);
let (logphi, logplo) = compute(&props[1]);
let (mrhi, mrlo) = compute(&props[2]);
let (mwhi, mwlo) = compute(&props[3]);
Bcut2D {
chghi,
chglo,
logphi,
logplo,
mrhi,
mrlo,
mwhi,
mwlo,
}
}
#[allow(clippy::needless_range_loop)] fn jacobi_eigenvalues(mat: &[Vec<f64>]) -> Vec<f64> {
let n = mat.len();
if n == 0 {
return Vec::new();
}
let mut a: Vec<Vec<f64>> = mat.to_vec();
for _ in 0..100 {
let (mut p, mut q, mut max_val) = (0, 1, 0.0f64);
for i in 0..n {
for j in (i + 1)..n {
if a[i][j].abs() > max_val {
max_val = a[i][j].abs();
p = i;
q = j;
}
}
}
if max_val < 1e-12 {
break;
}
let (app, aqq, apq) = (a[p][p], a[q][q], a[p][q]);
let tau = (aqq - app) / (2.0 * apq);
let t = if tau >= 0.0 {
1.0 / (tau + (1.0 + tau * tau).sqrt())
} else {
-1.0 / (-tau + (1.0 + tau * tau).sqrt())
};
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
a[p][p] = app - t * apq;
a[q][q] = aqq + t * apq;
a[p][q] = 0.0;
a[q][p] = 0.0;
for i in 0..n {
if i != p && i != q {
let (aip, aiq) = (a[i][p], a[i][q]);
a[i][p] = c * aip - s * aiq;
a[p][i] = a[i][p];
a[i][q] = s * aip + c * aiq;
a[q][i] = a[i][q];
}
}
}
(0..n).map(|i| a[i][i]).collect()
}
#[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, 2, "aspirin rotatable bonds = {r}");
}
#[test]
fn test_rot_alkyne_adjacent_excluded() {
let m = mol("CC#C");
assert_eq!(
rotatable_bond_count(&m),
0,
"propyne: C-C adj to triple bond excluded"
);
let m2 = mol("CCC#C");
assert_eq!(
rotatable_bond_count(&m2),
0,
"but-1-yne: both bonds excluded"
);
let m3 = mol("CCCC#C");
assert_eq!(
rotatable_bond_count(&m3),
1,
"pent-1-yne: CH2-CH2 bond is rotatable"
);
}
#[test]
fn test_rot_allene_excluded() {
let m = mol("C=C=C");
assert_eq!(rotatable_bond_count(&m), 0, "allene: no rotatable bonds");
}
#[test]
fn test_tpsa_water() {
let m = mol("O");
let t = tpsa(&m);
assert!(approx(t, 31.50, 0.1), "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_tpsa_quaternary_n_zero() {
let m = mol("c1csc([N+]2(C3CCCCC3)CCCCC2)c1");
let t = tpsa(&m);
assert!(t < 50.0, "N+ quaternary TPSA should be small, got {t}");
let m2 = mol("c1ccsc1");
let t_base = tpsa(&m2); assert!(
(t - t_base).abs() < 1.0,
"N+ adds ~0 to TPSA, got delta {}",
t - t_base
);
}
#[test]
fn test_tpsa_n_oxide_ionic() {
let m = mol("O=C1CC[N+]2([O-])CCCC[C@@H]12"); let t = tpsa(&m);
assert!(t > 0.0 && t < 150.0, "N-oxide TPSA in range, got {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_mqn_length() {
let m = mol("CCO");
let desc = mqn(&m);
assert_eq!(desc.len(), 42);
}
#[test]
fn test_mqn_single_carbon() {
let m = mol("C");
let desc = mqn(&m);
assert_eq!(desc.len(), 42);
assert_eq!(desc[0], 1); }
#[test]
fn test_mqn_ethane() {
let m = mol("CC");
let desc = mqn(&m);
assert_eq!(desc[0], 2); assert_eq!(desc[10], 1); assert_eq!(desc[37], 2); }
#[test]
fn test_mqn_benzene() {
let m = mol("c1ccccc1");
let desc = mqn(&m);
assert_eq!(desc[0], 6); assert_eq!(desc[13], 6); assert_eq!(desc[14], 1); assert_eq!(desc[15], 1); }
#[test]
fn test_mqn_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
let desc = mqn(&m);
assert_eq!(desc.len(), 42);
assert_eq!(desc[1], 0); assert!(desc[2] > 3); assert!(desc[37] > 12); }
#[test]
fn test_autocorr_2d_single_atom() {
let m = mol("C");
let ac = autocorr_2d(&m);
assert_eq!(ac.len(), 7);
for val in ac {
assert!((val - 0.0).abs() < 1e-9);
}
}
#[test]
fn test_autocorr_2d_ethane() {
let m = mol("CC");
let ac = autocorr_2d(&m);
assert_eq!(ac.len(), 7);
assert!((ac[0] - 16.0).abs() < 1e-9, "lag 1: {}", ac[0]);
for (i, value) in ac.iter().enumerate().take(7).skip(1) {
assert!((*value - 0.0).abs() < 1e-9, "lag {}: {}", i + 1, value);
}
}
#[test]
fn test_autocorr_2d_propane() {
let m = mol("CCC");
let ac = autocorr_2d(&m);
assert_eq!(ac.len(), 7);
assert!((ac[0] - 32.0).abs() < 1e-9, "lag 1: {}", ac[0]);
assert!((ac[1] - 16.0).abs() < 1e-9, "lag 2: {}", ac[1]);
}
#[test]
fn test_autocorr_2d_benzene() {
let m = mol("c1ccccc1");
let ac = autocorr_2d(&m);
assert_eq!(ac.len(), 7);
assert!((ac[0] - 54.0).abs() < 1e-9, "lag 1 benzene: {}", ac[0]);
assert!(ac[1] > 0.0, "lag 2 should be non-zero");
}
#[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_aromatic_ring_count_anthracene() {
let m = mol("c1ccc2cc3ccccc3cc2c1");
assert_eq!(aromatic_ring_count(&m), 3);
}
#[test]
fn test_aromatic_ring_count_pyrene() {
let m = mol("c1ccc2cccc3ccc4cccc1c4c23");
assert_eq!(aromatic_ring_count(&m), 4);
}
#[test]
fn test_aromatic_ring_count_triphenylene() {
let m = mol("c1ccc2ccc3ccccc3c2c1");
assert_eq!(aromatic_ring_count(&m), 3);
}
#[test]
fn test_aromatic_ring_count_fluoranthene() {
let m = mol("c1ccc2-c3cccc4cccc-3c4c2c1");
assert_eq!(aromatic_ring_count(&m), 4);
}
#[test]
fn test_aromatic_ring_count_acridine() {
let m = mol("c1ccc2nc3ccccc3cc2c1");
assert_eq!(aromatic_ring_count(&m), 3);
}
#[test]
fn test_arc_bench_lactone_benzene() {
let m = mol("CC1(C)CC(=O)c2c(ccc(C(=O)CC(N)CO)c2N)O1");
assert_eq!(aromatic_ring_count(&m), 1);
}
#[test]
fn test_arc_bench_bridged_benzenes() {
let m = mol("Cc1c(C)c2c(c(C)c1O)Oc1c(C)c([C@H](C)[C@@H](C)O)c(O)c(O)c1C2");
assert_eq!(aromatic_ring_count(&m), 2);
}
#[test]
fn test_arc_bench_no_bridge() {
let m = mol("C[C@]12Nc3ccccc3O[C@@]1(C)Nc1ccccc1O2");
assert_eq!(aromatic_ring_count(&m), 2);
}
#[test]
fn test_arc_bench_steroid_benzene() {
let m = mol(
"CC1=C2C[C@@]3(C)C[C@H](O)[C@](C)(C[C@H](O)[C@H](O)[C@@](C)(O)CO)[C@H]3c3ccc(C)c(c32)C1",
);
assert_eq!(aromatic_ring_count(&m), 1);
}
#[test]
fn test_hba_metformin() {
let m = mol("CN(C)C(=N)NC(=N)N");
assert_eq!(hba_count(&m), 2, "metformin HBA should match RDKit (2)");
}
#[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);
}
#[test]
fn test_num_stereocenters_bridgehead_quaternary() {
assert_eq!(
num_stereocenters(&mol("NS(=O)(=O)OC[C@@]12CCCC[C@@H]1CCC2")),
2
);
}
#[test]
fn test_num_stereocenters_ring_adjacent() {
assert_eq!(
num_stereocenters(&mol(
"CCCCc1cn([C@H]2[C@H](C)CCC[C@@H]2C)c(=O)n1Cc1ccc(-c2ccccc2-c2nn[nH]n2)nc1"
)),
3
);
}
#[test]
fn test_balaban_j_ethane() {
let m = mol("CC");
let bj = balaban_j(&m);
assert!(bj > 0.0, "ethane should have positive BalabanJ");
}
#[test]
fn test_balaban_j_benzene() {
let m = mol("c1ccccc1");
let bj = balaban_j(&m);
assert!(bj > 0.0, "benzene should have positive BalabanJ");
}
#[test]
fn test_balaban_j_single_atom_zero() {
let m = mol("C");
let bj = balaban_j(&m);
assert_eq!(bj, 0.0, "single atom should have BalabanJ = 0");
}
#[test]
fn test_balaban_j_matches_rdkit() {
assert!(
(balaban_j(&mol("c1ccccc1")) - 3.0).abs() < 1e-6,
"benzene BalabanJ"
);
assert!(
(balaban_j(&mol("C1CCCCC1")) - 2.0).abs() < 1e-6,
"cyclohexane BalabanJ"
);
assert!(
(balaban_j(&mol("CCCCCC")) - 2.3391).abs() < 0.001,
"hexane BalabanJ"
);
assert!(
(balaban_j(&mol("CCO")) - 1.6330).abs() < 0.001,
"ethanol BalabanJ"
);
}
#[test]
fn test_ipc_ethane() {
let m = mol("CC");
let ipc_val = ipc(&m);
assert!(ipc_val >= 0.0, "ethane should have non-negative Ipc");
}
#[test]
fn test_ipc_benzene() {
let m = mol("c1ccccc1");
let ipc_val = ipc(&m);
assert!(ipc_val > 0.0, "benzene should have positive Ipc");
}
#[test]
fn test_ipc_single_atom_zero() {
let m = mol("C");
let ipc_val = ipc(&m);
assert_eq!(ipc_val, 0.0, "single atom should have Ipc = 0");
}
#[test]
fn test_hall_kier_alpha_methane() {
let m = mol("C");
assert!(
(hall_kier_alpha(&m) - 0.0).abs() < 1e-9,
"methane HallKierAlpha should be exactly 0 (sp3 C is the reference atom)"
);
}
#[test]
fn test_hall_kier_alpha_ethane() {
let m = mol("CC");
assert!(
(hall_kier_alpha(&m) - 0.0).abs() < 1e-9,
"ethane (all sp3 C) HallKierAlpha should be exactly 0"
);
}
#[test]
fn test_hall_kier_alpha_benzene() {
let m = mol("c1ccccc1");
let hka = hall_kier_alpha(&m);
assert!(
hka < 0.0,
"benzene HallKierAlpha should be negative, got {hka}"
);
}
#[test]
fn test_usrcat_shape() {
let m = mol("CC");
let usr = usrcat(&m);
assert_eq!(usr.len(), 42, "USRCAT should return 42 values");
assert!(usr[0] >= 0.0, "first slot should be non-negative");
}
#[test]
fn test_usrcat_donors_acceptors() {
let m = mol("CCO");
let usr = usrcat(&m);
assert!(usr[36] >= 0.0, "donor count should be non-negative");
assert!(
usr[37] > 0.0,
"acceptor count should be positive (O present)"
);
}
#[test]
fn test_usrcat_aromatic() {
let m = mol("c1ccccc1");
let usr = usrcat(&m);
assert!(
usr[38] > 0.0,
"aromatic count should be positive for benzene"
);
}
#[test]
fn test_usrcat_charged() {
let m = mol("CC(=O)[O-]");
let usr = usrcat(&m);
assert!(
usr[40] > 0.0,
"anion count should be positive for charged carboxylate"
);
}
#[test]
fn test_mmff94_charges_length() {
let m = mol("CCO");
let charges = mmff94_charges(&m);
assert_eq!(charges.len(), 3, "should have 3 charges for 3 atoms");
}
#[test]
fn test_mmff94_charges_ethane() {
let m = mol("CC");
let charges = mmff94_charges(&m);
assert_eq!(charges.len(), 2);
assert!(
(charges[0] - charges[1]).abs() < 0.1,
"carbons in ethane should have similar charges"
);
}
#[test]
fn test_mmff94_charges_charged_species() {
let m = mol("CC(=O)[O-]");
let charges = mmff94_charges(&m);
assert_eq!(charges.len(), 4);
assert!(charges[3] < 0.0, "carboxylate oxygen should be negative");
}
#[test]
fn test_mmff94_charges_water() {
let m = mol("O");
let charges = mmff94_charges(&m);
assert_eq!(charges.len(), 1);
assert!(
charges[0].is_finite(),
"water oxygen charge should be finite"
);
}
#[test]
fn test_num_carbons_ethane() {
let m = mol("CC");
assert_eq!(num_carbons(&m), 2);
}
#[test]
fn test_num_nitrogens_methylamine() {
let m = mol("CN");
assert_eq!(num_nitrogens(&m), 1);
}
#[test]
fn test_num_oxygens_methanol() {
let m = mol("CO");
assert_eq!(num_oxygens(&m), 1);
}
#[test]
fn test_num_halogens() {
let m = mol("CCF");
assert_eq!(num_carbons(&m), 2);
assert_eq!(num_fluorines(&m), 1);
}
#[test]
fn test_num_hydrogens_methane() {
let m = mol("C");
assert_eq!(num_hydrogens(&m), 4);
}
#[test]
fn test_num_hydrogens_ethane() {
let m = mol("CC");
assert_eq!(num_hydrogens(&m), 6);
}
#[test]
fn test_num_hydrogens_water() {
let m = mol("O");
assert_eq!(num_hydrogens(&m), 2);
}
#[test]
fn test_calc_mol_formula_ethane() {
let m = mol("CC");
assert_eq!(calc_mol_formula(&m), "C2H6");
}
#[test]
fn test_num_bridgehead_atoms_acyclic() {
let m = mol("CCC");
assert_eq!(num_bridgehead_atoms(&m), 0);
}
#[test]
fn test_num_bridgehead_atoms_single_ring() {
let m = mol("C1CCCCC1");
assert_eq!(num_bridgehead_atoms(&m), 0);
}
#[test]
fn test_num_bridgehead_atoms_norbornane() {
let m = mol("C1CC2CCC1C2");
assert_eq!(num_bridgehead_atoms(&m), 2);
}
#[test]
fn test_num_bridgehead_atoms_naphthalene_fused() {
let m = mol("c1ccc2ccccc2c1");
assert_eq!(
num_bridgehead_atoms(&m),
0,
"naphthalene is fused, not bridged"
);
}
#[test]
fn test_num_spiro_atoms_single_ring() {
let m = mol("C1CCCCC1");
assert_eq!(num_spiro_atoms(&m), 0);
}
#[test]
fn test_calc_mol_formula_water() {
let m = mol("O");
assert_eq!(calc_mol_formula(&m), "H2O");
}
#[test]
fn test_calc_mol_formula_benzene() {
let m = mol("c1ccccc1");
assert_eq!(calc_mol_formula(&m), "C6H6");
}
#[test]
fn test_calc_mol_formula_acetic_acid() {
let m = mol("CC(=O)O");
assert_eq!(calc_mol_formula(&m), "C2H4O2");
}
#[test]
fn test_num_amide_bonds_acetamide() {
let m = mol("CC(=O)N");
assert_eq!(num_amide_bonds(&m), 1);
}
#[test]
fn test_num_amide_bonds_urea() {
let m = mol("NC(=O)N");
assert_eq!(num_amide_bonds(&m), 2);
}
#[test]
fn test_num_amide_bonds_primary_amide() {
let m = mol("CC(=O)N");
assert_eq!(num_amide_bonds(&m), 1);
}
#[test]
fn test_num_amide_bonds_none() {
let m = mol("c1ccccc1");
assert_eq!(num_amide_bonds(&m), 0);
}
#[test]
fn test_num_ester_bonds_methyl_formate() {
let m = mol("COC=O");
assert_eq!(num_ester_bonds(&m), 1);
}
#[test]
fn test_num_ester_bonds_acetic_acid_methyl_ester() {
let m = mol("CC(=O)OC");
assert_eq!(num_ester_bonds(&m), 1);
}
#[test]
fn test_num_ester_bonds_none() {
let m = mol("CC(=O)O");
assert_eq!(num_ester_bonds(&m), 0);
}
#[test]
fn test_num_ester_bonds_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert_eq!(
num_ester_bonds(&m),
1,
"aspirin has one ester bond (aryl ester)"
);
}
#[test]
fn test_logp_ethylene_terminal() {
let m = mol("C=C");
let lp = logp_crippen(&m);
assert!(lp > 0.3 && lp < 1.3, "ethylene logp = {lp}");
}
#[test]
fn test_logp_propene_terminal_internal() {
let m = mol("CC=C");
let lp = logp_crippen(&m);
let eth = logp_crippen(&mol("C=C"));
assert!(
lp > eth,
"propene logp ({lp}) should exceed ethylene ({eth})"
);
assert!(
lp > 0.5 && lp < 2.0,
"propene logp = {lp} out of expected range"
);
}
#[test]
fn test_logp_styrene_splits_correctly() {
let m = mol("C=Cc1ccccc1");
let lp = logp_crippen(&m);
assert!(lp > 1.8 && lp < 3.4, "styrene logp = {lp}");
let per_atom = logp_crippen_per_atom(&m);
assert!(per_atom.len() >= 8, "styrene has 8 heavy atoms");
}
#[test]
fn test_logp_1_phenylpropene_ar_adjacent() {
let m = mol("CC=Cc1ccccc1");
let lp = logp_crippen(&m);
assert!(lp > 2.0 && lp < 4.0, "1-phenylpropene logp = {lp}");
}
#[test]
fn test_logp_curcumin_reference() {
let m = mol("COc1cc(/C=C/C(=O)CC(=O)/C=C/c2ccc(O)c(OC)c2)ccc1O");
let lp = logp_crippen(&m);
assert!(lp > -5.0 && lp < 5.0, "curcumin crippen logp = {lp}");
}
#[test]
fn test_logp_complex_molecules_xlogp3_preferred() {
let m = mol("COc1cc(/C=C/C(=O)CC(=O)/C=C/c2ccc(O)c(OC)c2)ccc1O");
let crippen = logp_crippen(&m);
let xl3 = crate::xlogp3::xlogp3(&m);
assert!(crippen.is_finite(), "crippen logp must be finite");
assert!(xl3.is_finite(), "xlogp3 must be finite");
}
#[test]
fn test_logp_mvk_enone_vinyl() {
let m = mol("C=CC(=O)C");
let lp = logp_crippen(&m);
assert!(lp > 0.0 && lp < 1.5, "MVK logp = {lp}");
}
#[test]
fn test_logp_chalcone_enone() {
let m = mol("c1ccccc1/C=C/C(=O)c1ccccc1");
let lp = logp_crippen(&m);
assert!(lp > 1.8 && lp < 4.5, "chalcone logp = {lp}");
}
#[test]
fn test_logp_crotonate_internal_enone() {
let m = mol("CC=CC(=O)O");
let lp = logp_crippen(&m);
assert!(lp > -0.5 && lp < 1.5, "crotonate logp = {lp}");
}
#[test]
fn test_logp_enone_vs_plain_alkene() {
let enone = logp_crippen(&mol("C=CC(=O)C")); let alkene = logp_crippen(&mol("C=CCC")); assert!(
enone < alkene,
"MVK ({enone:.4}) should be < 1-butene ({alkene:.4}): enone is less hydrophobic"
);
}
#[test]
fn all_crippen_smarts_parse() {
let mut failed = Vec::new();
for (i, &(pattern, _, _)) in CRIPPEN_SMARTS.iter().enumerate() {
if parse_smarts(pattern).is_err() {
failed.push((i, pattern));
}
}
assert!(
failed.is_empty(),
"CRIPPEN_SMARTS parse failures (index, pattern): {:?}",
failed
);
}
#[test]
fn test_ro3_ethanol_passes() {
assert!(ro3_passes(&mol("CCO")));
}
#[test]
fn test_ro3_lipinski_drug_fails() {
assert!(!ro3_passes(&mol("CC(C)Cc1ccc(cc1)C(C)C(=O)O")));
}
#[test]
fn test_lead_like_ibuprofen_passes() {
assert!(lead_like_passes(&mol("CC(C)Cc1ccc(cc1)C(C)C(=O)O")));
}
#[test]
fn test_lead_like_large_drug_fails() {
let big = mol("CC(C)c1ccc(cc1)C(=O)NC2CCN(CC2)c3ncnc4c3ccc(c4)OC(F)(F)F");
assert!(!lead_like_passes(&big));
}
#[test]
fn test_pfizer_3_75_safe_compound_passes() {
assert!(pfizer_3_75_passes(&mol("CC(=O)Oc1ccccc1C(=O)O")));
}
#[test]
fn test_pfizer_3_75_risky_compound_fails() {
assert!(!pfizer_3_75_passes(&mol("CC(C)Cc1ccc(cc1)C(C)C(=O)O")));
}
#[test]
fn test_cns_mpo_score_range() {
for smi in [
"c1ccccc1",
"CC(=O)Oc1ccccc1C(=O)O",
"CC(C)Cc1ccc(cc1)C(C)C(=O)O",
"c1ccc2[nH]cnc2c1",
] {
let s = cns_mpo_score(&mol(smi));
assert!(
(0.0..=6.0).contains(&s),
"CNS MPO out of range for {smi}: {s}"
);
}
}
#[test]
fn test_cns_mpo_small_cns_drug_high_score() {
let s = cns_mpo_score(&mol("Cn1cnc2c1c(=O)n(c(=O)n2C)C"));
assert!(s >= 3.0, "caffeine CNS MPO should be ≥ 3, got {s}");
}
#[test]
fn test_tpsa_per_atom_sum_equals_tpsa() {
for smi in [
"CC(=O)Oc1ccccc1C(=O)O",
"Cn1cnc2c1c(=O)n(c(=O)n2C)C",
"c1ccncc1",
] {
let m = mol(smi);
let per_atom = tpsa_per_atom(&m);
assert_eq!(per_atom.len(), m.atom_count());
let sum: f64 = per_atom.iter().sum();
let direct = tpsa(&m);
assert!(
(sum - direct).abs() < 1e-9,
"tpsa_per_atom sum {sum} ≠ tpsa {direct} for {smi}"
);
}
}
#[test]
fn test_tpsa_per_atom_carbon_contributes_zero() {
let benz = mol("c1ccccc1");
for v in tpsa_per_atom(&benz) {
assert_eq!(v, 0.0);
}
}
#[test]
fn test_mcf_caffeine_passes() {
assert!(mcf_passes(&mol("Cn1cnc2c1c(=O)n(c(=O)n2C)C")));
}
#[test]
fn test_mcf_toluene_passes() {
assert!(mcf_passes(&mol("Cc1ccccc1")));
}
#[test]
fn test_mcf_ibuprofen_fails_brenk_acetal_ketal() {
assert!(!mcf_passes(&mol("CC(C)Cc1ccc(cc1)C(C)C(=O)O")));
}
#[test]
fn test_mcf_rhodanine_fails_pains() {
assert!(!mcf_passes(&mol("O=C1CSC(=S)N1")));
}
#[test]
fn test_mcf_aspirin_fails_brenk_active_ester() {
assert!(!mcf_passes(&mol("CC(=O)Oc1ccccc1C(=O)O")));
}
#[test]
fn test_mcf_very_large_mol_fails_lipinski() {
let big = mol("CC(C)c1ccc(cc1)C(=O)NC2CCN(CC2)c3ncnc4c3ccc(c4)OC(F)(F)F");
assert!(!mcf_passes(&big));
}
#[test]
fn test_hybridization_per_atom_ethane_all_sp3() {
let m = mol("CC");
let h = hybridization_per_atom(&m);
assert_eq!(h.len(), 2);
assert!(h.iter().all(|&v| v == 3), "both C in ethane → sp3: {h:?}");
}
#[test]
fn test_hybridization_per_atom_benzene_all_sp2() {
let m = mol("c1ccccc1");
let h = hybridization_per_atom(&m);
assert_eq!(h.len(), 6);
assert!(h.iter().all(|&v| v == 2), "all C in benzene → sp2: {h:?}");
}
#[test]
fn test_hybridization_per_atom_acetylene_sp() {
let m = mol("C#C");
let h = hybridization_per_atom(&m);
assert_eq!(h.len(), 2);
assert!(h.iter().all(|&v| v == 1), "alkyne C → sp: {h:?}");
}
#[test]
fn test_hybridization_per_atom_acetaldehyde_mixed() {
let m = mol("CC=O");
let h = hybridization_per_atom(&m);
assert_eq!(h.len(), 3); assert_eq!(h[0], 3, "methyl C → sp3");
assert_eq!(h[1], 2, "carbonyl C → sp2 (double bond to O)");
assert_eq!(h[2], 2, "carbonyl O → sp2 (double bond to C)");
}
#[test]
fn test_formal_charge_per_atom_neutral() {
let m = mol("CC(=O)O");
let fc = formal_charge_per_atom(&m);
assert_eq!(fc.len(), m.atom_count());
assert!(
fc.iter().all(|&c| c == 0),
"acetic acid has no formal charges: {fc:?}"
);
}
#[test]
fn test_formal_charge_per_atom_charged() {
let m = mol("[NH4+]");
let fc = formal_charge_per_atom(&m);
assert_eq!(fc.len(), 1, "only N is heavy");
assert_eq!(fc[0], 1, "N has formal charge +1");
}
#[test]
fn test_implicit_hcount_per_atom_ethane() {
let m = mol("CC");
let ih = implicit_hcount_per_atom(&m);
assert_eq!(ih.len(), 2);
assert!(
ih.iter().all(|&h| h == 3),
"each CH3 → 3 implicit H: {ih:?}"
);
}
#[test]
fn test_implicit_hcount_per_atom_sum() {
for smi in [
"CC(=O)O",
"c1ccccc1",
"CC(C)C",
"Cn1cnc2c1c(=O)n(c(=O)n2C)C",
] {
let m = mol(smi);
let ih = implicit_hcount_per_atom(&m);
assert_eq!(ih.len(), m.atom_count(), "length mismatch for {smi}");
}
}
#[test]
fn test_hba_count_lipinski_aspirin() {
assert_eq!(hba_count_lipinski(&mol("CC(=O)Oc1ccccc1C(=O)O")), 4);
}
#[test]
fn test_hba_count_lipinski_aniline() {
assert_eq!(hba_count_lipinski(&mol("Nc1ccccc1")), 1);
}
#[test]
fn test_hba_count_lipinski_ge_ertl() {
let acetamide = mol("CC(=O)N");
assert_eq!(hba_count_lipinski(&acetamide), 2); assert!(hba_count(&acetamide) <= hba_count_lipinski(&acetamide));
}
#[test]
fn test_fraction_rotatable_bonds_benzene() {
assert_eq!(fraction_rotatable_bonds(&mol("c1ccccc1")), 0.0);
}
#[test]
fn test_fraction_rotatable_bonds_in_range() {
for smi in ["CCCCc1ccccc1", "CC(=O)Oc1ccccc1C(=O)O", "CCCC"] {
let f = fraction_rotatable_bonds(&mol(smi));
assert!(
(0.0..=1.0).contains(&f),
"fraction out of [0,1] for {smi}: {f}"
);
}
}
#[test]
fn test_ring_system_count_benzene() {
assert_eq!(ring_system_count(&mol("c1ccccc1")), 1);
}
#[test]
fn test_ring_system_count_naphthalene_one_system() {
assert_eq!(ring_system_count(&mol("c1ccc2ccccc2c1")), 1);
}
#[test]
fn test_ring_system_count_biphenyl_two_systems() {
assert_eq!(ring_system_count(&mol("c1ccc(-c2ccccc2)cc1")), 2);
}
#[test]
fn test_ring_system_count_acyclic_zero() {
assert_eq!(ring_system_count(&mol("CCC")), 0);
}
#[test]
fn moran_len_is_7() {
assert_eq!(moran_autocorr(&mol("c1ccccc1")).len(), 7);
}
#[test]
fn geary_len_is_7() {
assert_eq!(geary_autocorr(&mol("c1ccccc1")).len(), 7);
}
#[test]
fn moran_single_atom_returns_zeros() {
let v = moran_autocorr(&mol("C"));
assert_eq!(v, vec![0.0; 7]);
}
#[test]
fn geary_single_atom_returns_ones() {
let v = geary_autocorr(&mol("C"));
assert_eq!(v, vec![1.0; 7]);
}
#[test]
fn moran_uniform_valence_is_zero() {
let v = moran_autocorr(&mol("c1ccccc1"));
for &x in &v {
assert!(x.abs() < 1e-9, "expected 0 for uniform valence, got {x}");
}
}
#[test]
fn geary_finite_for_mixed_molecule() {
let v = geary_autocorr(&mol("CCO"));
assert!(
v.iter().all(|x| x.is_finite()),
"all Geary values must be finite: {v:?}"
);
}
#[test]
fn moran_finite_for_mixed_molecule() {
let v = moran_autocorr(&mol("CCO"));
assert!(
v.iter().all(|x| x.is_finite()),
"all Moran values must be finite: {v:?}"
);
}
#[test]
fn carbon_types_methane() {
let ct = carbon_types(&mol("C"));
assert_eq!(
ct.c1sp3 + ct.c2sp3 + ct.c3sp3,
0,
"isolated methane C has deg=0, not counted"
);
}
#[test]
fn carbon_types_ethane() {
let ct = carbon_types(&mol("CC"));
assert_eq!(ct.c1sp3, 2, "ethane: 2×C1SP3");
assert_eq!(ct.c2sp3, 0);
}
#[test]
fn carbon_types_propane() {
let ct = carbon_types(&mol("CCC"));
assert_eq!(ct.c1sp3, 2, "propane: 2 terminal sp3 C");
assert_eq!(ct.c2sp3, 1, "propane: 1 internal sp3 C");
}
#[test]
fn carbon_types_benzene() {
let ct = carbon_types(&mol("c1ccccc1"));
assert_eq!(ct.c2sp2, 6, "benzene: 6×C2SP2 (aromatic)");
}
#[test]
fn carbon_types_acetylene() {
let ct = carbon_types(&mol("C#C"));
assert_eq!(ct.c1sp1, 2, "acetylene: 2×C1SP1");
}
#[test]
fn information_content_benzene_zero_ic() {
let ic = information_content(&mol("c1ccccc1"));
assert!(
ic.ic.abs() < 1e-9,
"benzene: IC must be 0 (fully symmetric), got {}",
ic.ic
);
assert!(ic.sic.abs() < 1e-9, "benzene: SIC must be 0");
}
#[test]
fn information_content_propane_nonzero() {
let ic = information_content(&mol("CCC"));
assert!(
ic.ic > 0.5,
"propane: IC must be > 0 (two symmetry classes)"
);
assert!(ic.tic > 0.0);
assert!(ic.ic.is_finite() && ic.tic.is_finite());
}
#[test]
fn information_content_single_atom() {
let ic = information_content(&mol("[Na+]"));
assert_eq!(ic.ic, 0.0);
assert_eq!(ic.tic, 0.0);
}
#[test]
fn mde_carbon_propane_mdec11() {
let v = mde_carbon(&mol("CCC"));
let expected = 1.0 / (2.0_f64).sqrt();
assert!(
(v[0] - expected).abs() < 1e-6,
"MDEC11 for propane: expected {expected:.4}, got {:.4}",
v[0]
);
}
#[test]
fn mde_carbon_all_finite() {
let v = mde_carbon(&mol("CC(CC)C(=O)O"));
assert!(v.iter().all(|&x| x.is_finite()), "all MDEC must be finite");
}
#[test]
fn bcut2d_hi_ge_lo() {
let b = bcut2d(&mol("c1ccccc1"));
assert!(b.mwhi >= b.mwlo, "BCUT2D-MWHI must be ≥ BCUT2D-MWLO");
assert!(b.logphi >= b.logplo, "BCUT2D-LOGPHI ≥ BCUT2D-LOGPLO");
assert!(b.mrhi >= b.mrlo, "BCUT2D-MRHI ≥ BCUT2D-MRLO");
}
#[test]
fn bcut2d_hi_strictly_greater_than_lo() {
let b = bcut2d(&mol("CC(=O)Oc1ccccc1C(=O)O"));
assert!(
b.mwhi > b.mwlo,
"MW hi/lo must differ: {} vs {}",
b.mwhi,
b.mwlo
);
assert!(
b.logphi > b.logplo,
"LogP hi/lo must differ: {} vs {}",
b.logphi,
b.logplo
);
assert!(
b.mrhi > b.mrlo,
"MR hi/lo must differ: {} vs {}",
b.mrhi,
b.mrlo
);
assert!(
b.chghi > b.chglo,
"Charge hi/lo must differ: {} vs {}",
b.chghi,
b.chglo
);
}
#[test]
fn bcut2d_all_finite() {
let b = bcut2d(&mol("CC(=O)Nc1ccccc1C(=O)O")); let vals = [
b.chghi, b.chglo, b.logphi, b.logplo, b.mrhi, b.mrlo, b.mwhi, b.mwlo,
];
assert!(
vals.iter().all(|v| v.is_finite()),
"all BCUT2D values must be finite: {vals:?}"
);
}
#[test]
fn bcut2d_single_atom_zero() {
let b = bcut2d(&mol("[H][H]"));
assert_eq!(b.mwhi, 0.0);
}
#[test]
fn logp_and_mr_matches_individual_functions() {
let smiles = [
"C", "c1ccccc1", "CC(=O)Oc1ccccc1C(=O)O", "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", "c1ccc2c(c1)cc1ccc3cccc4ccc2c1c34", "O=C(O)c1ccccc1O", "CCO", "CC(N)Cc1ccccc1", ];
for smi in smiles {
let m = mol(smi);
let expected_logp = logp_crippen(&m);
let expected_mr = molar_refractivity(&m);
let (got_logp, got_mr) = logp_and_mr(&m);
assert!(
(expected_logp - got_logp).abs() < 1e-9,
"{smi}: logp_crippen={expected_logp:.6} vs logp_and_mr.0={got_logp:.6}"
);
assert!(
(expected_mr - got_mr).abs() < 1e-9,
"{smi}: molar_refractivity={expected_mr:.6} vs logp_and_mr.1={got_mr:.6}"
);
}
}
}