#![cfg(feature = "sparse")]
use elasticrab::{Atom, NormalModes, Params};
mod common;
use common::{read_ca_pdb, read_eigenvalues};
fn ubiquitin() -> Vec<Atom> {
read_ca_pdb("1ubi_ca.pdb")
}
fn partial(atoms: &[Atom], k: usize, mass_weighted: bool) -> NormalModes {
let mut params = Params::default();
params.k_modes = Some(k);
params.mass_weighted = mass_weighted;
NormalModes::new(atoms, ¶ms).unwrap()
}
fn blocks_of(n_atoms: usize, size: usize) -> Vec<usize> {
(0..n_atoms).map(|i| i / size).collect()
}
fn lowest_nonzero(modes: &NormalModes, k: usize) -> Vec<f64> {
modes
.eigenvalues()
.iter()
.filter(|&&v| v.abs() > 1e-6)
.take(k)
.copied()
.collect()
}
#[test]
fn matrixfree_rtb_matches_dense_rtb() {
let atoms = ubiquitin();
let blocks = blocks_of(atoms.len(), 4); let k = 10;
let dense = NormalModes::with_blocks(&atoms, &blocks, &Params::default()).unwrap();
let dense_nonzero = lowest_nonzero(&dense, k);
let mut params = Params::default();
params.k_modes = Some(k);
let mf = NormalModes::with_blocks(&atoms, &blocks, ¶ms).unwrap();
assert_eq!(mf.len(), k);
for (got, want) in mf.eigenvalues().iter().zip(&dense_nonzero) {
assert!(
(got - want).abs() < 1e-5 * want.max(1.0),
"matrix-free {got:e} vs dense RTB {want:e}"
);
}
assert!(mf.eigenvalues().iter().all(|&v| v > 1e-6));
}
#[test]
fn matrixfree_all_singleton_matches_all_atom_partial() {
let atoms = ubiquitin();
let singletons: Vec<usize> = (0..atoms.len()).collect();
let k = 10;
let mut params = Params::default();
params.k_modes = Some(k);
let rtb = NormalModes::with_blocks(&atoms, &singletons, ¶ms).unwrap();
let all_atom = NormalModes::new(&atoms, ¶ms).unwrap();
for (a, b) in rtb.eigenvalues().iter().zip(all_atom.eigenvalues()) {
assert!((a - b).abs() < 1e-5 * b.max(1.0), "{a:e} vs {b:e}");
}
}
#[test]
fn matrixfree_rtb_mass_weighted() {
let mut atoms = ubiquitin();
for (i, a) in atoms.iter_mut().enumerate() {
a.mass = 12.0 + (i % 3) as f64;
}
let blocks = blocks_of(atoms.len(), 5);
let k = 8;
let mut dense_params = Params::default();
dense_params.mass_weighted = true;
let dense = NormalModes::with_blocks(&atoms, &blocks, &dense_params).unwrap();
let dense_nonzero = lowest_nonzero(&dense, k);
let mut params = dense_params;
params.k_modes = Some(k);
let mf = NormalModes::with_blocks(&atoms, &blocks, ¶ms).unwrap();
for (got, want) in mf.eigenvalues().iter().zip(&dense_nonzero) {
assert!(
(got - want).abs() < 1e-5 * want.max(1.0),
"mass-weighted matrix-free {got:e} vs dense {want:e}"
);
}
}
#[test]
fn sparse_matches_dense() {
let atoms = ubiquitin();
let k = 12;
let dense = NormalModes::new(&atoms, &Params::default()).unwrap();
let dense_nonzero: Vec<f64> = dense
.eigenvalues()
.iter()
.filter(|&&v| v.abs() > 1e-6)
.take(k)
.copied()
.collect();
let sparse = partial(&atoms, k, false);
assert_eq!(sparse.len(), k);
for (got, want) in sparse.eigenvalues().iter().zip(&dense_nonzero) {
assert!(
(got - want).abs() < 1e-6,
"sparse {got:e} vs dense {want:e}"
);
}
assert!(sparse.eigenvalues().iter().all(|&v| v > 1e-6));
}
#[test]
fn sparse_matches_prody() {
let atoms = ubiquitin();
let k = 12;
let reference = read_eigenvalues("anm1ubi_evalues.dat");
let modes = partial(&atoms, k, false);
for (i, &got) in modes.eigenvalues().iter().enumerate() {
let want = reference[6 + i];
assert!((got - want).abs() < 1e-4, "mode {i}: {got:e} vs {want:e}");
}
}
#[test]
fn sparse_mass_weighted_matches_dense() {
let mut atoms = ubiquitin();
for (i, a) in atoms.iter_mut().enumerate() {
a.mass = 12.0 + (i % 3) as f64; }
let k = 8;
let mut dense_params = Params::default();
dense_params.mass_weighted = true;
let dense = NormalModes::new(&atoms, &dense_params).unwrap();
let dense_nonzero: Vec<f64> = dense
.eigenvalues()
.iter()
.filter(|&&v| v.abs() > 1e-8)
.take(k)
.copied()
.collect();
let sparse = partial(&atoms, k, true);
for (got, want) in sparse.eigenvalues().iter().zip(&dense_nonzero) {
assert!(
(got - want).abs() < 1e-7,
"sparse {got:e} vs dense {want:e}"
);
}
}
#[test]
fn k_larger_than_spectrum_clamps() {
let atoms = ubiquitin();
let dof = 3 * atoms.len();
let modes = partial(&atoms, dof + 100, false);
assert!(modes.len() <= dof - 6);
assert!(!modes.is_empty());
}
#[test]
fn sparse_drops_disconnected_atom() {
let mut atoms = ubiquitin();
let isolated = atoms.len();
atoms.push(Atom {
position: [9999.0, 9999.0, 9999.0],
mass: 1.0,
});
let modes = partial(&atoms, 10, false);
assert_eq!(modes.disconnected(), &[isolated]);
assert_eq!(modes.len(), 10);
let reference = partial(&ubiquitin(), 10, false);
for (got, want) in modes.eigenvalues().iter().zip(reference.eigenvalues()) {
assert!(
(got - want).abs() < 1e-5 * want.max(1.0),
"with isolated {got:e} vs connected {want:e}"
);
}
}