use std::collections::{HashMap, HashSet, VecDeque};
use std::f64::consts::PI;
use chematic_core::{AtomIdx, BondOrder, Molecule, implicit_hcount};
fn heavy_indices(mol: &Molecule) -> Vec<usize> {
mol.atoms()
.filter(|(_, a)| a.element.atomic_number() != 1)
.map(|(idx, _)| idx.0 as usize)
.collect()
}
fn bfs_from(mol: &Molecule, start: usize, heavy_set: &HashSet<usize>) -> Vec<usize> {
let n = mol.atom_count();
let mut dist = vec![usize::MAX; n];
dist[start] = 0;
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(cur) = queue.pop_front() {
let d = dist[cur];
for (nb, _) in mol.neighbors(AtomIdx(cur as u32)) {
let ni = nb.0 as usize;
if heavy_set.contains(&ni) && dist[ni] == usize::MAX {
dist[ni] = d + 1;
queue.push_back(ni);
}
}
}
dist
}
fn delta(mol: &Molecule, idx: AtomIdx, heavy_set: &HashSet<usize>) -> f64 {
mol.neighbors(idx)
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as f64
}
fn delta_v(mol: &Molecule, idx: AtomIdx) -> f64 {
let atom = mol.atom(idx);
let z = atom.element.atomic_number() as i32;
let zv = valence_electrons(atom.element.atomic_number()) as i32;
let h = implicit_hcount(mol, idx) as i32 + atom.hydrogen_count.unwrap_or(0) as i32;
let denom = z - zv - 1;
if denom <= 0 {
return (zv - h).max(1) as f64;
}
((zv - h).max(0)) as f64 / denom as f64
}
fn valence_electrons(z: u8) -> u8 {
match z {
1 => 1,
2 => 2,
3 => 1,
4 => 2,
5 => 3,
6 => 4,
7 => 5,
8 => 6,
9 => 7,
10 => 8,
11 => 1,
12 => 2,
13 => 3,
14 => 4,
15 => 5,
16 => 6,
17 => 7,
18 => 8,
35 => 7,
53 => 7,
_ => z.min(8),
}
}
#[allow(clippy::too_many_arguments)]
fn chi_dfs(
mol: &Molecule,
cur: usize,
target_len: usize,
cur_len: usize,
running_product: f64,
visited: &mut Vec<bool>,
heavy_set: &HashSet<usize>,
use_valence: bool,
) -> f64 {
if cur_len == target_len {
return running_product.powf(-0.5);
}
let mut sum = 0.0f64;
for (nb, _) in mol.neighbors(AtomIdx(cur as u32)) {
let ni = nb.0 as usize;
if heavy_set.contains(&ni) && !visited[ni] {
let d_nb = if use_valence {
delta_v(mol, AtomIdx(ni as u32))
} else {
delta(mol, AtomIdx(ni as u32), heavy_set)
};
if d_nb > 0.0 {
visited[ni] = true;
sum += chi_dfs(
mol,
ni,
target_len,
cur_len + 1,
running_product * d_nb,
visited,
heavy_set,
use_valence,
);
visited[ni] = false;
}
}
}
sum
}
fn count_paths(mol: &Molecule, heavy: &[usize], length: usize) -> usize {
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
let mut total = 0usize;
for &start in heavy {
let mut visited = vec![false; mol.atom_count()];
visited[start] = true;
total += count_paths_dfs(mol, start, length, 0, &mut visited, &heavy_set);
}
total / 2
}
fn count_paths_dfs(
mol: &Molecule,
cur: usize,
target_len: usize,
cur_len: usize,
visited: &mut Vec<bool>,
heavy_set: &HashSet<usize>,
) -> usize {
if cur_len == target_len {
return 1;
}
let mut count = 0;
for (nb, _) in mol.neighbors(AtomIdx(cur as u32)) {
let ni = nb.0 as usize;
if heavy_set.contains(&ni) && !visited[ni] {
visited[ni] = true;
count += count_paths_dfs(mol, ni, target_len, cur_len + 1, visited, heavy_set);
visited[ni] = false;
}
}
count
}
fn chi_n(mol: &Molecule, n: usize, use_valence: bool) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
let mut total = 0.0f64;
for &start in &heavy {
let d_start = if use_valence {
delta_v(mol, AtomIdx(start as u32))
} else {
delta(mol, AtomIdx(start as u32), &heavy_set)
};
if d_start <= 0.0 {
continue;
}
let mut visited = vec![false; mol.atom_count()];
visited[start] = true;
total += chi_dfs(
mol,
start,
n,
0,
d_start,
&mut visited,
&heavy_set,
use_valence,
);
}
total / 2.0
}
pub fn wiener_index(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
let mut sum = 0u64;
for i in 0..heavy.len() {
let row = bfs_from(mol, heavy[i], &heavy_set);
for j in (i + 1)..heavy.len() {
let d = row[heavy[j]];
if d != usize::MAX {
sum += d as u64;
}
}
}
sum as f64
}
pub fn kappa1(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n < 2 {
return 0.0;
}
let p1 = count_paths(mol, &heavy, 1);
if p1 == 0 {
return 0.0;
}
let n = n as f64;
let p1 = p1 as f64;
n * (n - 1.0).powi(2) / p1.powi(2)
}
pub fn kappa2(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n < 3 {
return 0.0;
}
let p2 = count_paths(mol, &heavy, 2);
if p2 == 0 {
return 0.0;
}
let n = n as f64;
let p2 = p2 as f64;
(n - 1.0) * (n - 2.0).powi(2) / p2.powi(2)
}
pub fn kappa3(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n < 4 {
return 0.0;
}
let p3 = count_paths(mol, &heavy, 3);
if p3 == 0 {
return 0.0;
}
let n_f = n as f64;
let p3 = p3 as f64;
let factor = if n % 2 == 1 { n_f - 1.0 } else { n_f - 2.0 };
factor * (n_f - 3.0).powi(2) / p3.powi(2)
}
pub fn chi0(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
heavy
.iter()
.map(|&i| {
let d = delta(mol, AtomIdx(i as u32), &heavy_set);
if d > 0.0 { d.powf(-0.5) } else { 0.0 }
})
.sum()
}
pub fn chi1(mol: &Molecule) -> f64 {
chi_n(mol, 1, false)
}
pub fn chi2(mol: &Molecule) -> f64 {
chi_n(mol, 2, false)
}
pub fn chi3(mol: &Molecule) -> f64 {
chi_n(mol, 3, false)
}
pub fn chi4(mol: &Molecule) -> f64 {
chi_n(mol, 4, false)
}
pub fn chi0v(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
heavy
.iter()
.map(|&i| {
let d = delta_v(mol, AtomIdx(i as u32));
if d > 0.0 { d.powf(-0.5) } else { 0.0 }
})
.sum()
}
pub fn chi1v(mol: &Molecule) -> f64 {
chi_n(mol, 1, true)
}
pub fn chi2v(mol: &Molecule) -> f64 {
chi_n(mol, 2, true)
}
pub fn chi3v(mol: &Molecule) -> f64 {
chi_n(mol, 3, true)
}
pub fn chi4v(mol: &Molecule) -> f64 {
chi_n(mol, 4, true)
}
pub fn bertz_ct(mol: &Molecule) -> f64 {
let mut total_h_bonds = 0u64;
let mut complexity = 0.0f64;
for (idx, _) in mol.atoms() {
let heavy_deg = mol.degree(idx);
let h = implicit_hcount(mol, idx) as usize;
total_h_bonds += h as u64;
let total_deg = heavy_deg + h;
complexity += (total_deg * total_deg.saturating_sub(1) / 2) as f64;
}
let heavy_bonds = mol.bond_count() as u64;
let m_total = heavy_bonds + total_h_bonds;
complexity + m_total as f64
}
fn rb0(atomic_number: u8) -> f64 {
match atomic_number {
1 => 0.33, 6 => 0.77, 7 => 0.70, 8 => 0.66, 9 => 0.611, 14 => 1.04, 15 => 0.89, 16 => 1.04, 17 => 0.997, 33 => 1.21, 34 => 1.20, 35 => 1.167, 53 => 1.387, _ => 0.0,
}
}
fn bond_scale(order: BondOrder) -> f64 {
match order {
BondOrder::Aromatic => 0.1,
BondOrder::Single
| BondOrder::Up
| BondOrder::Down
| BondOrder::Zero
| BondOrder::Dative
| BondOrder::QueryAny
| BondOrder::QuerySingleOrDouble
| BondOrder::QuerySingleOrAromatic
| BondOrder::QueryDoubleOrAromatic => 0.0,
BondOrder::Double => 0.2,
BondOrder::Triple | BondOrder::Quadruple => 0.3,
}
}
pub fn labute_asa_per_atom(mol: &Molecule) -> Vec<f64> {
let n = mol.atom_count();
if n == 0 {
return Vec::new();
}
const R_H: f64 = 0.33;
let mut v: Vec<f64> = vec![0.0; n];
let radii: Vec<f64> = (0..n)
.map(|i| rb0(mol.atom(AtomIdx(i as u32)).element.atomic_number()))
.collect();
for (_, bond) in mol.bonds() {
let i = bond.atom1.0 as usize;
let j = bond.atom2.0 as usize;
let ri = radii[i];
let rj = radii[j];
if ri < 1e-10 || rj < 1e-10 {
continue;
}
let scale = bond_scale(bond.order);
let bij = ri + rj - scale;
let dij = (ri - rj).abs().max(bij).min(ri + rj);
let vi = (rj * rj - (ri - dij) * (ri - dij)) / dij;
let vj = (ri * ri - (rj - dij) * (rj - dij)) / dij;
if vi > 0.0 {
v[i] += vi;
}
if vj > 0.0 {
v[j] += vj;
}
}
for i in 0..n {
let ri = radii[i];
if ri < 1e-10 {
continue;
}
let h_count = implicit_hcount(mol, AtomIdx(i as u32)) as usize;
for _ in 0..h_count {
let dij = ri + R_H;
let vi = (R_H * R_H - (ri - dij) * (ri - dij)) / dij;
if vi > 0.0 {
v[i] += vi;
}
}
}
(0..n)
.map(|i| {
let ri = radii[i];
let h_count = implicit_hcount(mol, AtomIdx(i as u32)) as usize;
let heavy_area = (4.0 * PI * ri * ri - PI * ri * v[i]).max(0.0);
heavy_area + h_count as f64 * 4.0 * PI * R_H * R_H
})
.collect()
}
pub fn randic_index(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
let mut sum = 0.0f64;
for i in 0..mol.bond_count() {
let bond = mol.bond(chematic_core::BondIdx(i as u32));
let a = bond.atom1.0 as usize;
let b = bond.atom2.0 as usize;
if !heavy_set.contains(&a) || !heavy_set.contains(&b) {
continue; }
let da = mol
.neighbors(bond.atom1)
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as f64;
let db = mol
.neighbors(bond.atom2)
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as f64;
if da > 0.0 && db > 0.0 {
sum += 1.0 / (da * db).sqrt();
}
}
sum
}
pub fn zagreb_index_m1(mol: &Molecule) -> u32 {
let heavy = heavy_indices(mol);
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
heavy
.iter()
.map(|&i| {
let deg = mol
.neighbors(chematic_core::AtomIdx(i as u32))
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as u32;
deg * deg
})
.sum()
}
pub fn zagreb_index_m2(mol: &Molecule) -> u32 {
let heavy_set: HashSet<usize> = (0..mol.atom_count())
.filter(|&i| {
mol.atom(chematic_core::AtomIdx(i as u32))
.element
.atomic_number()
!= 1
})
.collect();
let mut sum = 0u32;
for bidx in 0..mol.bond_count() {
let bond = mol.bond(chematic_core::BondIdx(bidx as u32));
let a = bond.atom1.0 as usize;
let b = bond.atom2.0 as usize;
if !heavy_set.contains(&a) || !heavy_set.contains(&b) {
continue;
}
let deg_a = mol
.neighbors(bond.atom1)
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as u32;
let deg_b = mol
.neighbors(bond.atom2)
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as u32;
sum += deg_a * deg_b;
}
sum
}
pub fn topological_distance_matrix(mol: &Molecule) -> Vec<Vec<u32>> {
let heavy = heavy_indices(mol);
let heavy_set: HashSet<usize> = heavy.iter().copied().collect();
let n = heavy.len();
let mut pos_of: HashMap<usize, usize> = HashMap::new();
for (p, &h) in heavy.iter().enumerate() {
pos_of.insert(h, p);
}
let mut matrix = vec![vec![u32::MAX; n]; n];
for p in 0..n {
matrix[p][p] = 0;
let row = bfs_from(mol, heavy[p], &heavy_set);
for q in 0..n {
let d = row[heavy[q]];
if d != usize::MAX {
matrix[p][q] = d as u32;
}
}
}
matrix
}
pub fn labute_asa(mol: &Molecule) -> f64 {
labute_asa_per_atom(mol).iter().sum()
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn mol(s: &str) -> Molecule {
parse(s).unwrap_or_else(|e| panic!("parse '{s}': {e}"))
}
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn wiener_ethane() {
assert_eq!(wiener_index(&mol("CC")) as u32, 1);
}
#[test]
fn wiener_propane() {
assert_eq!(wiener_index(&mol("CCC")) as u32, 4);
}
#[test]
fn wiener_benzene() {
assert_eq!(wiener_index(&mol("c1ccccc1")) as u32, 27);
}
#[test]
fn wiener_increases_with_chain_length() {
let w2 = wiener_index(&mol("CC")); let w3 = wiener_index(&mol("CCC")); let w4 = wiener_index(&mol("CCCC")); assert!(w2 < w3 && w3 < w4);
}
#[test]
fn wiener_single_atom_zero() {
assert_eq!(wiener_index(&mol("C")) as u32, 0);
}
#[test]
fn kappa1_propane() {
assert!(close(kappa1(&mol("CCC")), 3.0, 0.01));
}
#[test]
fn kappa1_benzene() {
assert!(close(kappa1(&mol("c1ccccc1")), 4.167, 0.01));
}
#[test]
fn kappa2_propane() {
assert!(close(kappa2(&mol("CCC")), 2.0, 0.01));
}
#[test]
fn kappa2_benzene() {
assert!(close(kappa2(&mol("c1ccccc1")), 2.222, 0.01));
}
#[test]
fn kappa3_propane_zero() {
assert_eq!(kappa3(&mol("CCC")), 0.0);
}
#[test]
fn kappa3_benzene() {
assert!(close(kappa3(&mol("c1ccccc1")), 1.0, 0.01));
}
#[test]
fn kappa1_single_atom_zero() {
assert_eq!(kappa1(&mol("C")), 0.0);
}
#[test]
fn chi0_benzene() {
let c = chi0(&mol("c1ccccc1"));
assert!(close(c, 4.243, 0.01), "chi0(benzene) = {c}");
}
#[test]
fn chi1_benzene() {
let c = chi1(&mol("c1ccccc1"));
assert!(close(c, 3.0, 0.01), "chi1(benzene) = {c}");
}
#[test]
fn chi0_propane() {
let c = chi0(&mol("CCC"));
assert!(close(c, 2.707, 0.01), "chi0(propane) = {c}");
}
#[test]
fn chi1_propane() {
let c = chi1(&mol("CCC"));
assert!(close(c, 1.414, 0.01), "chi1(propane) = {c}");
}
#[test]
fn chi_increases_with_chain() {
assert!(chi0(&mol("CCC")) < chi0(&mol("CCCC")));
}
#[test]
fn chi0v_benzene() {
let c = chi0v(&mol("c1ccccc1"));
assert!(close(c, 3.464, 0.01), "chi0v(benzene) = {c}");
}
#[test]
fn chi1v_benzene() {
let c = chi1v(&mol("c1ccccc1"));
assert!(close(c, 2.0, 0.01), "chi1v(benzene) = {c}");
}
#[test]
fn bertz_ct_increases_with_complexity() {
let bz = bertz_ct(&mol("c1ccccc1"));
let asp = bertz_ct(&mol("CC(=O)Oc1ccccc1C(=O)O"));
assert!(bz < asp, "benzene BertzCT {bz} should be < aspirin {asp}");
}
#[test]
fn bertz_ct_ethane_less_than_propane() {
assert!(bertz_ct(&mol("CC")) < bertz_ct(&mol("CCC")));
}
#[test]
fn bertz_ct_methane() {
assert!(close(bertz_ct(&mol("C")), 10.0, 0.01));
}
#[test]
fn bertz_ct_benzene() {
assert!(close(bertz_ct(&mol("c1ccccc1")), 30.0, 0.01));
}
#[test]
fn labute_asa_positive() {
assert!(labute_asa(&mol("c1ccccc1")) > 0.0, "benzene ASA > 0");
}
#[test]
fn labute_asa_single_oxygen() {
let asa = labute_asa(&mol("O"));
let expected = 4.0 * PI * 0.66_f64.powi(2) + 2.0 * 4.0 * PI * 0.33_f64.powi(2);
assert!(
(asa - expected).abs() < 0.01,
"water ASA {asa:.4} ≠ expected {expected:.4}"
);
}
#[test]
fn labute_asa_monotone_with_size() {
let bz = labute_asa(&mol("c1ccccc1"));
let asp = labute_asa(&mol("CC(=O)Oc1ccccc1C(=O)O"));
assert!(
bz < asp,
"benzene ASA {bz:.2} should be < aspirin ASA {asp:.2}"
);
}
#[test]
fn labute_asa_aromatic_reduces_vs_saturated() {
let bz = labute_asa(&mol("c1ccccc1")); let ch = labute_asa(&mol("C1CCCCC1")); assert!(bz < ch, "benzene ASA {bz:.2} < cyclohexane ASA {ch:.2}");
}
#[test]
fn randic_index_ethane() {
let m = mol("CC");
assert!((randic_index(&m) - 1.0).abs() < 1e-9);
}
#[test]
fn zagreb_m1_ethane() {
let m = mol("CC");
assert_eq!(zagreb_index_m1(&m), 2);
}
#[test]
fn zagreb_m2_ethane() {
assert_eq!(zagreb_index_m2(&mol("CC")), 1);
}
#[test]
fn zagreb_m2_propane() {
assert_eq!(zagreb_index_m2(&mol("CCC")), 4);
}
#[test]
fn zagreb_m2_benzene() {
assert_eq!(zagreb_index_m2(&mol("c1ccccc1")), 24);
}
#[test]
fn zagreb_m2_ge_m1_for_branched() {
for smi in ["CC(C)C", "CC(=O)O", "c1ccccc1"] {
let m = mol(smi);
assert!(zagreb_index_m2(&m) > 0, "M2 should be > 0 for {smi}");
}
}
#[test]
fn distance_matrix_ethane() {
let m = mol("CC");
let dm = topological_distance_matrix(&m);
assert_eq!(dm.len(), 2);
assert_eq!(dm[0][0], 0);
assert_eq!(dm[0][1], 1);
assert_eq!(dm[1][0], 1);
assert_eq!(dm[1][1], 0);
}
#[test]
fn distance_matrix_propane() {
let m = mol("CCC");
let dm = topological_distance_matrix(&m);
assert_eq!(dm[0][2], 2);
assert_eq!(dm[1][2], 1);
}
}