use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::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 heavy_degrees(mol: &Molecule) -> Vec<u32> {
let n = mol.atom_count();
let is_heavy: Vec<bool> = (0..n)
.map(|i| mol.atom(AtomIdx(i as u32)).element.atomic_number() != 1)
.collect();
(0..n)
.map(|i| {
if !is_heavy[i] {
return 0;
}
mol.neighbors(AtomIdx(i as u32))
.filter(|(nb, _)| is_heavy[nb.0 as usize])
.count() as u32
})
.collect()
}
fn bfs_from(mol: &Molecule, start: usize, heavy_set: &FxHashSet<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: &FxHashSet<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: &FxHashSet<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: FxHashSet<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: &FxHashSet<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_with(
mol: &Molecule,
heavy: &[usize],
heavy_set: &FxHashSet<usize>,
n: usize,
use_valence: bool,
) -> f64 {
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
}
fn chi_n(mol: &Molecule, n: usize, use_valence: bool) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
chi_n_with(mol, &heavy, &heavy_set, n, use_valence)
}
pub fn wiener_index(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<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 padmakar_ivan_index(mol: &Molecule) -> u64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n < 2 {
return 0;
}
if n > 1000 {
return u64::MAX;
}
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let mut pos: FxHashMap<usize, usize> =
FxHashMap::with_capacity_and_hasher(n, Default::default());
for (p, &h) in heavy.iter().enumerate() {
pos.insert(h, p);
}
let mut dist = vec![vec![usize::MAX; n]; n];
for p in 0..n {
dist[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 {
dist[p][q] = d;
}
}
}
let mut pi_val = 0u64;
for (_, bond) in mol.bonds() {
let u = bond.atom1.0 as usize;
let v = bond.atom2.0 as usize;
if !heavy_set.contains(&u) || !heavy_set.contains(&v) {
continue;
}
let pu = pos[&u];
let pv = pos[&v];
let mut n_u = 0u64;
let mut n_v = 0u64;
for (du, dv) in dist[pu].iter().zip(dist[pv].iter()) {
if du < dv {
n_u += 1;
} else if dv < du {
n_v += 1;
}
}
pi_val += n_u + n_v;
}
pi_val
}
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 alpha = crate::descriptors::hall_kier_alpha(mol);
let a = n as f64 + alpha;
let p1 = p1 as f64 + alpha;
a * (a - 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 alpha = crate::descriptors::hall_kier_alpha(mol);
let a = n as f64 + alpha;
let p2 = p2 as f64 + alpha;
(a - 1.0) * (a - 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 alpha = crate::descriptors::hall_kier_alpha(mol);
let a = n as f64 + alpha;
let p3 = p3 as f64 + alpha;
let factor = if n % 2 == 1 { a - 1.0 } else { a - 2.0 };
factor * (a - 3.0).powi(2) / p3.powi(2)
}
pub fn kappa_all(mol: &Molecule) -> (f64, f64, f64) {
let heavy = heavy_indices(mol);
let n = heavy.len();
let alpha = crate::descriptors::hall_kier_alpha(mol);
let a = n as f64 + alpha;
let k1 = if n >= 2 {
let p1 = count_paths(mol, &heavy, 1);
if p1 == 0 {
0.0
} else {
let p1 = p1 as f64 + alpha;
a * (a - 1.0).powi(2) / p1.powi(2)
}
} else {
0.0
};
let k2 = if n >= 3 {
let p2 = count_paths(mol, &heavy, 2);
if p2 == 0 {
0.0
} else {
let p2 = p2 as f64 + alpha;
(a - 1.0) * (a - 2.0).powi(2) / p2.powi(2)
}
} else {
0.0
};
let k3 = if n >= 4 {
let p3 = count_paths(mol, &heavy, 3);
if p3 == 0 {
0.0
} else {
let p3 = p3 as f64 + alpha;
let factor = if n % 2 == 1 { a - 1.0 } else { a - 2.0 };
factor * (a - 3.0).powi(2) / p3.powi(2)
}
} else {
0.0
};
(k1, k2, k3)
}
pub fn chi0(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<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 chi_all(mol: &Molecule) -> (f64, f64, f64, f64, f64, f64, f64, f64, f64, f64) {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let c0 = 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();
let c0v = 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();
(
c0,
chi_n_with(mol, &heavy, &heavy_set, 1, false),
chi_n_with(mol, &heavy, &heavy_set, 2, false),
chi_n_with(mol, &heavy, &heavy_set, 3, false),
chi_n_with(mol, &heavy, &heavy_set, 4, false),
c0v,
chi_n_with(mol, &heavy, &heavy_set, 1, true),
chi_n_with(mol, &heavy, &heavy_set, 2, true),
chi_n_with(mol, &heavy, &heavy_set, 3, true),
chi_n_with(mol, &heavy, &heavy_set, 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,
}
}
fn labute_asa_parts(mol: &Molecule) -> (Vec<f64>, f64) {
let n = mol.atom_count();
if n == 0 {
return (Vec::new(), 0.0);
}
const R_H: f64 = 0.33;
let mut v: Vec<f64> = vec![0.0; n];
let mut h_pool = 0.0f64;
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);
v[i] += rj * rj - (ri - dij) * (ri - dij) / dij;
v[j] += ri * ri - (rj - dij) * (rj - dij) / dij;
}
for i in 0..n {
let ri = radii[i];
if ri < 1e-10 {
continue;
}
let dij = ri + R_H;
v[i] += R_H * R_H - (ri - dij) * (ri - dij) / dij;
h_pool += ri * ri - (R_H - dij) * (R_H - dij) / dij;
}
let per_atom = (0..n)
.map(|i| {
let ri = radii[i];
if ri < 1e-10 {
return 0.0;
}
(4.0 * PI * ri * ri - PI * ri * v[i]).max(0.0)
})
.collect();
let h_pool_area = (4.0 * PI * R_H * R_H - PI * R_H * h_pool).max(0.0);
(per_atom, h_pool_area)
}
pub fn labute_asa_per_atom(mol: &Molecule) -> Vec<f64> {
labute_asa_parts(mol).0
}
#[cfg(test)]
pub(crate) fn labute_h_pool_area(mol: &Molecule) -> f64 {
labute_asa_parts(mol).1
}
pub fn randic_index(mol: &Molecule) -> f64 {
let deg = heavy_degrees(mol);
let mut sum = 0.0f64;
for i in 0..mol.bond_count() {
let bond = mol.bond(chematic_core::BondIdx(i as u32));
let da = deg[bond.atom1.0 as usize] as f64;
let db = deg[bond.atom2.0 as usize] as f64;
if da > 0.0 && db > 0.0 {
sum += 1.0 / (da * db).sqrt();
}
}
sum
}
pub fn zagreb_index_m1(mol: &Molecule) -> u32 {
heavy_degrees(mol).iter().map(|&d| d * d).sum()
}
pub fn zagreb_index_m2(mol: &Molecule) -> u32 {
let deg = heavy_degrees(mol);
let mut sum = 0u32;
for bidx in 0..mol.bond_count() {
let bond = mol.bond(chematic_core::BondIdx(bidx as u32));
let da = deg[bond.atom1.0 as usize];
let db = deg[bond.atom2.0 as usize];
if da > 0 && db > 0 {
sum += da * db;
}
}
sum
}
pub fn graph_eccentricities(mol: &Molecule) -> Vec<u32> {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
heavy
.iter()
.map(|&h| {
let row = bfs_from(mol, h, &heavy_set);
heavy
.iter()
.filter_map(|&h2| {
let d = row[h2];
if d == usize::MAX {
None
} else {
Some(d as u32)
}
})
.max()
.unwrap_or(0)
})
.collect()
}
pub fn graph_diameter(mol: &Molecule) -> u32 {
graph_eccentricities(mol).into_iter().max().unwrap_or(0)
}
pub fn graph_radius(mol: &Molecule) -> u32 {
graph_eccentricities(mol).into_iter().min().unwrap_or(0)
}
pub fn petitjean_index(mol: &Molecule) -> f64 {
let ecc = graph_eccentricities(mol);
if ecc.is_empty() {
return 0.0;
}
let d = ecc.iter().copied().max().unwrap_or(0);
let r = ecc.iter().copied().min().unwrap_or(0);
if d == 0 {
0.0
} else {
(d - r) as f64 / d as f64
}
}
pub fn eccentric_connectivity_index(mol: &Molecule) -> u64 {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let ecc = graph_eccentricities(mol);
heavy
.iter()
.zip(ecc.iter())
.map(|(&h, &e)| {
let deg = mol
.neighbors(AtomIdx(h as u32))
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as u64;
deg * e as u64
})
.sum()
}
pub fn hosoya_index(mol: &Molecule) -> u64 {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let n = heavy.len();
if n == 0 {
return 1; }
if n > 40 {
return 0; }
let pos_of: FxHashMap<usize, usize> = heavy.iter().enumerate().map(|(i, &h)| (h, i)).collect();
let mut adj = vec![vec![false; n]; n];
for (_, bond) in mol.bonds() {
let a = bond.atom1.0 as usize;
let b = bond.atom2.0 as usize;
if let (Some(&pa), Some(&pb)) = (pos_of.get(&a), pos_of.get(&b))
&& heavy_set.contains(&a)
&& heavy_set.contains(&b)
{
adj[pa][pb] = true;
adj[pb][pa] = true;
}
}
let mut available = vec![true; n];
count_matchings_hosoya(&adj, &mut available, n)
}
fn count_matchings_hosoya(adj: &[Vec<bool>], available: &mut Vec<bool>, n: usize) -> u64 {
let v = match available.iter().position(|&a| a) {
Some(v) => v,
None => return 1, };
available[v] = false;
let mut z = count_matchings_hosoya(adj, available, n);
for u in 0..n {
if adj[v][u] && available[u] {
available[u] = false;
z += count_matchings_hosoya(adj, available, n);
available[u] = true;
}
}
available[v] = true;
z
}
pub fn topological_distance_matrix(mol: &Molecule) -> Vec<Vec<u32>> {
let heavy = heavy_indices(mol);
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let n = heavy.len();
let mut pos_of: FxHashMap<usize, usize> = FxHashMap::default();
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 {
let (per_atom, h_pool_area) = labute_asa_parts(mol);
per_atom.iter().sum::<f64>() + h_pool_area
}
fn r_vdw_bondi(z: u8) -> f64 {
match z {
1 => 1.20,
5 => 1.80,
6 => 1.70,
7 => 1.55,
8 => 1.52,
9 => 1.47,
14 => 2.10,
15 => 1.80,
16 => 1.80,
17 => 1.75,
33 => 1.85,
34 => 1.90,
35 => 1.85,
53 => 1.98,
_ => 2.00,
}
}
fn sphere_intersection(r1: f64, r2: f64, d: f64) -> f64 {
if d <= 0.0 || d >= r1 + r2 {
return 0.0;
}
if d <= (r1 - r2).abs() {
let r_min = r1.min(r2);
return 4.0 / 3.0 * PI * r_min * r_min * r_min;
}
let h1 = r1 - (d * d + r1 * r1 - r2 * r2) / (2.0 * d);
let h2 = r2 - (d * d + r2 * r2 - r1 * r1) / (2.0 * d);
let cap = |r: f64, h: f64| {
if h <= 0.0 {
0.0
} else {
PI / 3.0 * h * h * (3.0 * r - h)
}
};
cap(r1, h1) + cap(r2, h2)
}
pub fn vabc(mol: &Molecule) -> f64 {
let n = mol.atom_count();
if n == 0 {
return 0.0;
}
const R_H_VDW: f64 = 1.20;
const R_H_COV: f64 = 0.33;
let sphere = |r: f64| 4.0 / 3.0 * PI * r * r * r;
let mut v = 0.0f64;
for (idx, atom) in mol.atoms() {
let z = atom.element.atomic_number();
v += sphere(r_vdw_bondi(z));
let nh = implicit_hcount(mol, idx) as usize;
v += nh as f64 * sphere(R_H_VDW);
}
for (_, bond) in mol.bonds() {
let z1 = mol.atom(bond.atom1).element.atomic_number();
let z2 = mol.atom(bond.atom2).element.atomic_number();
let rb1 = rb0(z1);
let rb2 = rb0(z2);
if rb1 > 1e-10 && rb2 > 1e-10 {
v -= sphere_intersection(r_vdw_bondi(z1), r_vdw_bondi(z2), rb1 + rb2);
}
}
for (idx, atom) in mol.atoms() {
let z = atom.element.atomic_number();
let rb_heavy = rb0(z);
if rb_heavy < 1e-10 {
continue;
}
let ov = sphere_intersection(r_vdw_bondi(z), R_H_VDW, rb_heavy + R_H_COV);
let nh = implicit_hcount(mol, idx) as usize;
v -= nh as f64 * ov;
}
v.max(0.0)
}
fn avg_mass_for_grav(z: u8) -> f64 {
match z {
1 => 1.008,
6 => 12.011,
7 => 14.007,
8 => 15.999,
9 => 18.998,
14 => 28.086,
15 => 30.974,
16 => 32.065,
17 => 35.453,
35 => 79.904,
53 => 126.904,
n => n as f64,
}
}
pub fn gravitational_index(mol: &Molecule) -> f64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n == 0 {
return 0.0;
}
let masses: Vec<f64> = heavy
.iter()
.map(|&a| avg_mass_for_grav(mol.atom(AtomIdx(a as u32)).element.atomic_number()))
.collect();
let dist = topological_distance_matrix(mol);
let mut g = 0.0f64;
for i in 0..n {
for j in (i + 1)..n {
let d = dist[i][j];
if d != u32::MAX && d > 0 {
g += masses[i] * masses[j] / (d as f64 * d as f64);
}
}
}
g
}
pub fn schultz_mti(mol: &Molecule) -> u64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n == 0 {
return 0;
}
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let deg: Vec<u64> = heavy
.iter()
.map(|&a| {
mol.neighbors(AtomIdx(a as u32))
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as u64
})
.collect();
let dist = topological_distance_matrix(mol);
let mut s = 0u64;
for i in 0..n {
for j in (i + 1)..n {
let d = dist[i][j];
if d < u32::MAX {
s += (deg[i] + deg[j]) * d as u64;
}
}
}
s
}
pub fn gutman_mti(mol: &Molecule) -> u64 {
let heavy = heavy_indices(mol);
let n = heavy.len();
if n == 0 {
return 0;
}
let heavy_set: FxHashSet<usize> = heavy.iter().copied().collect();
let deg: Vec<u64> = heavy
.iter()
.map(|&a| {
mol.neighbors(AtomIdx(a as u32))
.filter(|(nb, _)| heavy_set.contains(&(nb.0 as usize)))
.count() as u64
})
.collect();
let dist = topological_distance_matrix(mol);
let mut s = 0u64;
for i in 0..n {
for j in (i + 1)..n {
let d = dist[i][j];
if d < u32::MAX {
s += deg[i] * deg[j] * d as u64;
}
}
}
s
}
pub fn num_valence_electrons(mol: &Molecule) -> u32 {
mol.atoms()
.filter(|(_, a)| a.element.atomic_number() > 1)
.map(|(idx, a)| {
let heavy = u32::from(valence_electrons(a.element.atomic_number()));
let h = u32::from(implicit_hcount(mol, idx));
heavy + h })
.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")), 3.4116, 0.001));
}
#[test]
fn kappa2_propane() {
assert!(close(kappa2(&mol("CCC")), 2.0, 0.01));
}
#[test]
fn kappa2_benzene() {
assert!(close(kappa2(&mol("c1ccccc1")), 1.6058, 0.001));
}
#[test]
fn kappa3_propane_zero() {
assert_eq!(kappa3(&mol("CCC")), 0.0);
}
#[test]
fn kappa3_benzene() {
assert!(close(kappa3(&mol("c1ccccc1")), 0.5824, 0.001));
}
#[test]
fn kappa1_single_atom_zero() {
assert_eq!(kappa1(&mol("C")), 0.0);
}
#[test]
fn kappa_alpha_corrected_matches_rdkit_aspirin() {
let m = mol("CC(=O)Oc1ccccc1C(=O)O");
assert!(close(kappa1(&m), 9.2496, 0.1), "kappa1 = {}", kappa1(&m));
assert!(close(kappa2(&m), 3.7093, 0.1), "kappa2 = {}", kappa2(&m));
assert!(close(kappa3(&m), 2.2974, 0.1), "kappa3 = {}", kappa3(&m));
}
#[test]
fn kappa_all_matches_individual() {
for smi in ["CC", "CCC", "CCCC", "c1ccccc1", "CC(=O)Oc1ccccc1C(=O)O"] {
let m = mol(smi);
let (k1, k2, k3) = kappa_all(&m);
assert!((k1 - kappa1(&m)).abs() < 1e-10, "{smi}: kappa1 mismatch");
assert!((k2 - kappa2(&m)).abs() < 1e-10, "{smi}: kappa2 mismatch");
assert!((k3 - kappa3(&m)).abs() < 1e-10, "{smi}: kappa3 mismatch");
}
}
#[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"));
assert!(
(asa - 6.8492).abs() < 0.001,
"water ASA {asa:.4} != RDKit 6.8492"
);
}
#[test]
fn labute_asa_matches_rdkit_aspirin() {
let asa = labute_asa(&mol("CC(=O)Oc1ccccc1C(=O)O"));
assert!((asa - 74.7571).abs() < 0.001, "aspirin ASA {asa:.4}");
}
#[test]
fn labute_asa_per_atom_excludes_pooled_h_term() {
let m = mol("CC(C)(C)C");
let per_atom = labute_asa_per_atom(&m);
let expected = [6.9237, 5.4150, 6.9237, 6.9237, 6.9237];
for (got, want) in per_atom.iter().zip(expected.iter()) {
assert!((got - want).abs() < 0.001, "{per_atom:?} vs {expected:?}");
}
let total = labute_asa(&m);
assert!((total - (expected.iter().sum::<f64>() + 1.0891)).abs() < 0.001);
}
#[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 eccentricities_propane() {
let m = mol("CCC");
let ecc = graph_eccentricities(&m);
assert_eq!(ecc.len(), 3);
assert_eq!(*ecc.iter().max().unwrap(), 2);
assert_eq!(*ecc.iter().min().unwrap(), 1);
}
#[test]
fn graph_diameter_propane() {
assert_eq!(graph_diameter(&mol("CCC")), 2);
}
#[test]
fn graph_radius_propane() {
assert_eq!(graph_radius(&mol("CCC")), 1);
}
#[test]
fn petitjean_propane() {
let v = petitjean_index(&mol("CCC"));
assert!((v - 0.5).abs() < 1e-9, "expected 0.5 got {v}");
}
#[test]
fn petitjean_benzene() {
let v = petitjean_index(&mol("c1ccccc1"));
assert!(
(v - 0.0).abs() < 1e-9,
"expected 0.0 (symmetric ring) got {v}"
);
}
#[test]
fn petitjean_single_atom() {
assert_eq!(petitjean_index(&mol("C")), 0.0);
}
#[test]
fn eci_propane() {
assert_eq!(eccentric_connectivity_index(&mol("CCC")), 6);
}
#[test]
fn hosoya_methane() {
assert_eq!(hosoya_index(&mol("C")), 1);
}
#[test]
fn hosoya_ethane() {
assert_eq!(hosoya_index(&mol("CC")), 2);
}
#[test]
fn hosoya_propane() {
assert_eq!(hosoya_index(&mol("CCC")), 3);
}
#[test]
fn hosoya_butane() {
assert_eq!(hosoya_index(&mol("CCCC")), 5);
}
#[test]
fn hosoya_benzene() {
assert_eq!(hosoya_index(&mol("c1ccccc1")), 18);
}
#[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);
}
#[test]
fn pi_single_atom() {
assert_eq!(padmakar_ivan_index(&mol("C")), 0);
}
#[test]
fn pi_ethane() {
assert_eq!(padmakar_ivan_index(&mol("CC")), 2);
}
#[test]
fn pi_propane() {
assert_eq!(padmakar_ivan_index(&mol("CCC")), 6);
}
#[test]
fn pi_butane() {
assert_eq!(padmakar_ivan_index(&mol("CCCC")), 12);
}
#[test]
fn pi_benzene() {
assert_eq!(padmakar_ivan_index(&mol("c1ccccc1")), 36);
}
#[test]
fn pi_linear_chain_formula() {
for n in 2..=6 {
let smiles = "C".repeat(n);
let expected = (n as u64) * (n as u64 - 1);
assert_eq!(padmakar_ivan_index(&mol(&smiles)), expected, "chain n={n}");
}
}
#[test]
fn schultz_mti_ethane() {
assert_eq!(schultz_mti(&mol("CC")), 2);
}
#[test]
fn gutman_mti_ethane() {
assert_eq!(gutman_mti(&mol("CC")), 1);
}
#[test]
fn schultz_mti_propane() {
assert_eq!(schultz_mti(&mol("CCC")), 10);
}
#[test]
fn gutman_mti_propane() {
assert_eq!(gutman_mti(&mol("CCC")), 6);
}
#[test]
fn schultz_mti_empty() {
let m = chematic_smiles::parse("[C]").unwrap();
assert_eq!(schultz_mti(&m), 0);
}
#[test]
fn vabc_methane() {
let v = vabc(&mol("C"));
assert!(v > 10.0, "VABC(methane) should be > 10 ų, got {v}");
assert!(v < 100.0, "VABC(methane) should be < 100 ų, got {v}");
}
#[test]
fn vabc_water() {
let v = vabc(&mol("O"));
assert!(v > 5.0, "VABC(water) should be > 5 ų, got {v}");
assert!(v < 50.0, "VABC(water) should be < 50 ų, got {v}");
}
#[test]
fn vabc_increases_with_size() {
let v_ethane = vabc(&mol("CC"));
let v_propane = vabc(&mol("CCC"));
let v_butane = vabc(&mol("CCCC"));
assert!(v_propane > v_ethane, "propane > ethane");
assert!(v_butane > v_propane, "butane > propane");
}
#[test]
fn gravitational_index_single_atom() {
let m = chematic_smiles::parse("[C]").unwrap();
assert_eq!(gravitational_index(&m), 0.0);
}
#[test]
fn gravitational_index_ethane() {
let g = gravitational_index(&mol("CC"));
assert!((g - 12.011_f64 * 12.011).abs() < 0.01, "got {g}");
}
#[test]
fn gravitational_index_positive() {
let g = gravitational_index(&mol("c1ccccc1"));
assert!(g > 0.0, "benzene gravitational index should be positive");
}
#[test]
fn chi_all_matches_individual() {
for smi in [
"CC",
"CCC",
"c1ccccc1",
"CC(=O)Oc1ccccc1C(=O)O",
"CN1C=NC2=C1C(=O)N(C)C(=O)N2C",
] {
let m = mol(smi);
let (c0, c1, c2, c3, c4, c0v, c1v, c2v, c3v, c4v) = chi_all(&m);
assert!((c0 - chi0(&m)).abs() < 1e-10, "{smi}: chi0 mismatch");
assert!((c1 - chi1(&m)).abs() < 1e-10, "{smi}: chi1 mismatch");
assert!((c2 - chi2(&m)).abs() < 1e-10, "{smi}: chi2 mismatch");
assert!((c3 - chi3(&m)).abs() < 1e-10, "{smi}: chi3 mismatch");
assert!((c4 - chi4(&m)).abs() < 1e-10, "{smi}: chi4 mismatch");
assert!((c0v - chi0v(&m)).abs() < 1e-10, "{smi}: chi0v mismatch");
assert!((c1v - chi1v(&m)).abs() < 1e-10, "{smi}: chi1v mismatch");
assert!((c2v - chi2v(&m)).abs() < 1e-10, "{smi}: chi2v mismatch");
assert!((c3v - chi3v(&m)).abs() < 1e-10, "{smi}: chi3v mismatch");
assert!((c4v - chi4v(&m)).abs() < 1e-10, "{smi}: chi4v mismatch");
}
}
}