use std::f64::consts::PI;
use crate::{AdjacencyList, Molecule};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum MolTransformError {
#[error("atom index {atom} out of bounds: num_atoms={num_atoms}")]
AtomIndexOutOfBounds { atom: usize, num_atoms: usize },
#[error("atoms must be bonded: {i} and {j}")]
AtomsNotBonded { i: usize, j: usize },
#[error("bond ({i},{j}) must not belong to a ring")]
BondInRing { i: usize, j: usize },
#[error("atoms {i} and {j} have identical 3D coordinates")]
IdenticalCoordinates { i: usize, j: usize },
#[error("no 3D conformer available for molecule")]
NoConformer3D,
#[error("conformer index {id} out of bounds: max={max}")]
ConformerIndexOutOfBounds { id: usize, max: usize },
#[error("bond ({i},{j}) and ({j},{k}) must not both belong to a ring")]
BondsBothInRing { i: usize, j: usize, k: usize },
#[error("atoms {i} and {j} have identical 3D coordinates (zero-length vector)")]
ZeroLengthVector { i: usize, j: usize },
}
fn conf_coords(mol: &Molecule, conf_id: usize) -> Result<&[[f64; 3]], MolTransformError> {
let confs = mol.conformers_3d();
let conf = confs
.get(conf_id)
.ok_or(MolTransformError::ConformerIndexOutOfBounds {
id: conf_id,
max: confs.len(),
})?;
Ok(conf.coords())
}
fn conf_coords_mut(
mol: &mut Molecule,
conf_id: usize,
) -> Result<&mut [[f64; 3]], MolTransformError> {
let coord_block = mol.coordinate_block_mut();
let n_confs = coord_block.conformers_3d.len();
let conf = coord_block.conformers_3d.get_mut(conf_id).ok_or(
MolTransformError::ConformerIndexOutOfBounds {
id: conf_id,
max: n_confs,
},
)?;
Ok(conf.coords_mut())
}
fn dot3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn length3(v: &[f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn length_sq3(v: &[f64; 3]) -> f64 {
v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
}
fn sub3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add3(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale3(v: &[f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
fn normalize3(v: &[f64; 3]) -> Option<[f64; 3]> {
let len = length3(v);
if len <= 1e-16 {
None
} else {
Some([v[0] / len, v[1] / len, v[2] / len])
}
}
fn angle_between(v1: &[f64; 3], v2: &[f64; 3]) -> f64 {
let dot = dot3(v1, v2);
let len1 = length3(v1);
let len2 = length3(v2);
let cos_ang = (dot / (len1 * len2)).clamp(-1.0, 1.0);
cos_ang.acos()
}
fn to_be_moved_idx_list(
adj: &crate::AdjacencyList,
n_atoms: usize,
i_atom: usize,
j_atom: usize,
) -> Vec<usize> {
let mut visited = vec![false; n_atoms];
let mut stack: Vec<usize> = Vec::new();
stack.push(j_atom);
visited[i_atom] = true;
visited[j_atom] = true;
while let Some(&t_idx) = stack.last() {
let mut found_unvisited = false;
for nbr in adj.neighbors_of(t_idx) {
let w_idx = nbr.atom_index;
if !visited[w_idx] {
visited[w_idx] = true;
stack.push(w_idx);
found_unvisited = true;
break;
}
}
if found_unvisited {
continue;
}
visited[t_idx] = true;
stack.pop();
}
let mut alist: Vec<usize> = Vec::new();
for i in 0..n_atoms {
if visited[i] && i != i_atom {
alist.push(i);
}
}
alist
}
pub fn get_bond_length(
mol: &Molecule,
i: usize,
j: usize,
conf_id: usize,
) -> Result<f64, MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
let num_atoms = coords.len();
if i >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: i, num_atoms });
}
if j >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: j, num_atoms });
}
Ok(length3(&sub3(&coords[i], &coords[j])))
}
fn set_bond_length(
mol: Molecule,
conf_id: usize,
i: usize,
j: usize,
value: f64,
) -> Result<Molecule, MolTransformError> {
let num_atoms = mol.num_atoms();
let adj = AdjacencyList::from_topology(num_atoms, mol.bonds());
if i >= num_atoms || j >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds {
atom: if i >= num_atoms { i } else { j },
num_atoms,
});
}
let adj = AdjacencyList::from_topology(num_atoms, mol.bonds());
let mut mol = mol;
let coords = conf_coords_mut(&mut mol, conf_id)?;
let v = sub3(&coords[i], &coords[j]);
let orig_value = length3(&v);
if orig_value <= 1e-8 {
return Err(MolTransformError::IdenticalCoordinates { i, j });
}
let scale = value / orig_value - 1.0;
let delta = scale3(&v, scale);
let alist = to_be_moved_idx_list(&adj, num_atoms, i, j);
for &idx in &alist {
let pt = &mut coords[idx];
pt[0] -= delta[0];
pt[1] -= delta[1];
pt[2] -= delta[2];
}
Ok(mol)
}
pub fn get_bond_angle(
mol: &Molecule,
i: usize,
j: usize,
k: usize,
conf_id: usize,
) -> Result<f64, MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
let num_atoms = coords.len();
if i >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: i, num_atoms });
}
if j >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: j, num_atoms });
}
if k >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: k, num_atoms });
}
let r_ji = sub3(&coords[i], &coords[j]);
let r_ji_sq = length_sq3(&r_ji);
if r_ji_sq <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i, j });
}
let r_jk = sub3(&coords[k], &coords[j]);
let r_jk_sq = length_sq3(&r_jk);
if r_jk_sq <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i: j, j: k });
}
Ok(angle_between(&r_ji, &r_jk))
}
pub fn get_bond_angle_deg(
mol: &Molecule,
i: usize,
j: usize,
k: usize,
conf_id: usize,
) -> Result<f64, MolTransformError> {
get_bond_angle(mol, i, j, k, conf_id).map(|rad| rad * 180.0 / PI)
}
pub fn set_bond_angle(
mol: Molecule,
i: usize,
j: usize,
k: usize,
value: f64,
conf_id: usize,
) -> Result<Molecule, MolTransformError> {
let num_atoms = mol.num_atoms();
let adj = AdjacencyList::from_topology(num_atoms, mol.bonds());
if i >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: i, num_atoms });
}
if j >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: j, num_atoms });
}
if k >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: k, num_atoms });
}
let bond_ji = mol.bonds().iter().any(|b| {
(b.begin().index() == j && b.end().index() == i)
|| (b.begin().index() == i && b.end().index() == j)
});
if !bond_ji {
return Err(MolTransformError::AtomsNotBonded { i: j, j: i });
}
let bond_jk = mol.bonds().iter().any(|b| {
(b.begin().index() == j && b.end().index() == k)
|| (b.begin().index() == k && b.end().index() == j)
});
if !bond_jk {
return Err(MolTransformError::AtomsNotBonded { i: j, j: k });
}
let mut mol = mol;
let coords = conf_coords_mut(&mut mol, conf_id)?;
let r_ji = sub3(&coords[i], &coords[j]);
if length_sq3(&r_ji) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i, j });
}
let r_jk = sub3(&coords[k], &coords[j]);
if length_sq3(&r_jk) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i: j, j: k });
}
let current_angle = angle_between(&r_ji, &r_jk);
let delta_angle = value - current_angle;
let rot_axis_begin = coords[j]; let rot_axis_raw = cross3(&r_ji, &r_jk);
let rot_axis = match normalize3(&rot_axis_raw) {
Some(axis) => axis,
None => return Err(MolTransformError::ZeroLengthVector { i: j, j: k }),
};
let alist = to_be_moved_idx_list(&adj, num_atoms, j, k);
for &idx in &alist {
coords[idx][0] -= rot_axis_begin[0];
coords[idx][1] -= rot_axis_begin[1];
coords[idx][2] -= rot_axis_begin[2];
rotate_point_around_axis(&mut coords[idx], &rot_axis, delta_angle);
coords[idx][0] += rot_axis_begin[0];
coords[idx][1] += rot_axis_begin[1];
coords[idx][2] += rot_axis_begin[2];
}
Ok(mol)
}
pub fn set_bond_angle_deg(
mol: Molecule,
i: usize,
j: usize,
k: usize,
value: f64,
conf_id: usize,
) -> Result<Molecule, MolTransformError> {
set_bond_angle(mol, i, j, k, value * PI / 180.0, conf_id)
}
pub fn get_dihedral(
mol: &Molecule,
i: usize,
j: usize,
k: usize,
l: usize,
conf_id: usize,
) -> Result<f64, MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
let num_atoms = coords.len();
if i >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: i, num_atoms });
}
if j >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: j, num_atoms });
}
if k >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: k, num_atoms });
}
if l >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: l, num_atoms });
}
let r_ij = sub3(&coords[j], &coords[i]);
if length_sq3(&r_ij) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i, j });
}
let r_jk = sub3(&coords[k], &coords[j]);
if length_sq3(&r_jk) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i: j, j: k });
}
let r_kl = sub3(&coords[l], &coords[k]);
if length_sq3(&r_kl) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i: k, j: l });
}
let n_ijk = cross3(&r_ij, &r_jk);
let n_ijk_sq = length_sq3(&n_ijk);
let n_jkl = cross3(&r_jk, &r_kl);
let n_jkl_sq = length_sq3(&n_jkl);
let m = cross3(&n_ijk, &r_jk);
let denom1 = (n_jkl_sq * length_sq3(&m)).sqrt();
let denom2 = (n_ijk_sq * n_jkl_sq).sqrt();
let angle = -f64::atan2(
dot3(&m, &n_jkl) / denom1.max(f64::MIN_POSITIVE),
dot3(&n_ijk, &n_jkl) / denom2.max(f64::MIN_POSITIVE),
);
Ok(angle)
}
pub fn get_dihedral_deg(
mol: &Molecule,
i: usize,
j: usize,
k: usize,
l: usize,
conf_id: usize,
) -> Result<f64, MolTransformError> {
get_dihedral(mol, i, j, k, l, conf_id).map(|rad| rad * 180.0 / PI)
}
pub fn set_dihedral(
mol: Molecule,
i: usize,
j: usize,
k: usize,
l: usize,
value: f64,
conf_id: usize,
) -> Result<Molecule, MolTransformError> {
let num_atoms = mol.num_atoms();
let adj = AdjacencyList::from_topology(num_atoms, mol.bonds());
if i >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: i, num_atoms });
}
if j >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: j, num_atoms });
}
if k >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: k, num_atoms });
}
if l >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom: l, num_atoms });
}
let bond_jk = mol.bonds().iter().any(|b| {
(b.begin().index() == j && b.end().index() == k)
|| (b.begin().index() == k && b.end().index() == j)
});
if !bond_jk {
return Err(MolTransformError::AtomsNotBonded { i: j, j: k });
}
let mut mol = mol;
let coords = conf_coords_mut(&mut mol, conf_id)?;
let r_ij = sub3(&coords[j], &coords[i]);
if length_sq3(&r_ij) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i, j });
}
let r_jk = sub3(&coords[k], &coords[j]);
if length_sq3(&r_jk) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i: j, j: k });
}
let r_kl = sub3(&coords[l], &coords[k]);
if length_sq3(&r_kl) <= 1e-16 {
return Err(MolTransformError::IdenticalCoordinates { i: k, j: l });
}
let n_ijk = cross3(&r_ij, &r_jk);
let n_ijk_sq = length_sq3(&n_ijk);
let n_jkl = cross3(&r_jk, &r_kl);
let n_jkl_sq = length_sq3(&n_jkl);
let m = cross3(&n_ijk, &r_jk);
let denom1 = (n_jkl_sq * length_sq3(&m)).sqrt();
let denom2 = (n_ijk_sq * n_jkl_sq).sqrt();
let current_dihedral = -f64::atan2(
dot3(&m, &n_jkl) / denom1.max(f64::MIN_POSITIVE),
dot3(&n_ijk, &n_jkl) / denom2.max(f64::MIN_POSITIVE),
);
let delta_angle = value - current_dihedral;
let rot_axis_begin = coords[j]; let rot_axis_end = coords[k]; let rot_axis_raw = sub3(&rot_axis_end, &rot_axis_begin);
let rot_axis = match normalize3(&rot_axis_raw) {
Some(axis) => axis,
None => return Err(MolTransformError::ZeroLengthVector { i: j, j: k }),
};
let alist = to_be_moved_idx_list(&adj, num_atoms, j, k);
for &idx in &alist {
coords[idx][0] -= rot_axis_begin[0];
coords[idx][1] -= rot_axis_begin[1];
coords[idx][2] -= rot_axis_begin[2];
rotate_point_around_axis(&mut coords[idx], &rot_axis, delta_angle);
coords[idx][0] += rot_axis_begin[0];
coords[idx][1] += rot_axis_begin[1];
coords[idx][2] += rot_axis_begin[2];
}
Ok(mol)
}
pub fn set_dihedral_deg(
mol: Molecule,
i: usize,
j: usize,
k: usize,
l: usize,
value: f64,
conf_id: usize,
) -> Result<Molecule, MolTransformError> {
set_dihedral(mol, i, j, k, l, value * PI / 180.0, conf_id)
}
fn rotate_point_around_axis(point: &mut [f64; 3], axis: &[f64; 3], angle: f64) {
let cos_a = angle.cos();
let sin_a = angle.sin();
let dot = dot3(point, axis);
let cross = cross3(axis, point);
point[0] = point[0] * cos_a + cross[0] * sin_a + axis[0] * dot * (1.0 - cos_a);
point[1] = point[1] * cos_a + cross[1] * sin_a + axis[1] * dot * (1.0 - cos_a);
point[2] = point[2] * cos_a + cross[2] * sin_a + axis[2] * dot * (1.0 - cos_a);
}
pub fn get_atom_position(
mol: &Molecule,
atom: usize,
conf_id: usize,
) -> Result<[f64; 3], MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
if atom >= coords.len() {
return Err(MolTransformError::AtomIndexOutOfBounds {
atom,
num_atoms: coords.len(),
});
}
Ok(coords[atom])
}
pub fn set_atom_position(
mol: Molecule,
atom: usize,
pos: [f64; 3],
conf_id: usize,
) -> Result<Molecule, MolTransformError> {
let num_atoms = mol.num_atoms();
let adj = AdjacencyList::from_topology(num_atoms, mol.bonds());
if atom >= num_atoms {
return Err(MolTransformError::AtomIndexOutOfBounds { atom, num_atoms });
}
let mut mol = mol;
let coords = conf_coords_mut(&mut mol, conf_id)?;
coords[atom] = pos;
Ok(mol)
}
pub fn get_atom_positions(
mol: &Molecule,
conf_id: usize,
) -> Result<Vec<[f64; 3]>, MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
Ok(coords.to_vec())
}
pub fn transform_conformer(
mol: Molecule,
transform: &[[f64; 4]; 4],
conf_id: usize,
) -> Result<Molecule, MolTransformError> {
let mut mol = mol;
let coords = conf_coords_mut(&mut mol, conf_id)?;
for pt in coords.iter_mut() {
let x = pt[0];
let y = pt[1];
let z = pt[2];
pt[0] = transform[0][0] * x + transform[0][1] * y + transform[0][2] * z + transform[0][3];
pt[1] = transform[1][0] * x + transform[1][1] * y + transform[1][2] * z + transform[1][3];
pt[2] = transform[2][0] * x + transform[2][1] * y + transform[2][2] * z + transform[2][3];
}
Ok(mol)
}
pub fn get_translation_vectors(
mol: &Molecule,
conf_id: usize,
) -> Result<Vec<[f64; 3]>, MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
Ok(coords.to_vec())
}
pub fn compute_centroid(
mol: &Molecule,
conf_id: usize,
ignore_hs: bool,
) -> Result<[f64; 3], MolTransformError> {
let coords = conf_coords(mol, conf_id)?;
let mut res = [0.0, 0.0, 0.0];
let mut count = 0usize;
for i in 0..coords.len() {
if ignore_hs && mol.atoms()[i].atomic_number() == 1 {
continue;
}
res[0] += coords[i][0];
res[1] += coords[i][1];
res[2] += coords[i][2];
count += 1;
}
if count > 0 {
let inv = 1.0 / count as f64;
res[0] *= inv;
res[1] *= inv;
res[2] *= inv;
}
Ok(res)
}
pub fn center_atoms_on_origin(
mol: Molecule,
conf_id: usize,
ignore_hs: bool,
) -> Result<Molecule, MolTransformError> {
let centroid = compute_centroid(&mol, conf_id, ignore_hs)?;
let mut mol = mol;
let coords = conf_coords_mut(&mut mol, conf_id)?;
for pt in coords.iter_mut() {
pt[0] -= centroid[0];
pt[1] -= centroid[1];
pt[2] -= centroid[2];
}
Ok(mol)
}