use super::blocks::{build_projection_matrix, detect_rigid_blocks};
#[derive(Debug, Clone)]
pub struct MbhConfig {
pub fd_step: f64,
}
impl Default for MbhConfig {
fn default() -> Self {
Self { fd_step: 0.005 }
}
}
#[derive(Debug, Clone)]
pub struct MbhResult {
pub frequencies: Vec<f64>,
pub n_blocks: usize,
pub n_flexible: usize,
pub n_dof_reduced: usize,
pub n_dof_full: usize,
pub speedup: f64,
}
fn atomic_mass(z: u8) -> f64 {
match z {
1 => 1.008,
5 => 10.81,
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,
_ => 12.011,
}
}
pub fn compute_mbh_frequencies(
elements: &[u8],
positions: &[[f64; 3]],
rings: &[(Vec<usize>, bool)],
energy_fn: &dyn Fn(&[f64]) -> f64,
config: &MbhConfig,
) -> MbhResult {
let n_atoms = elements.len();
let decomp = detect_rigid_blocks(n_atoms, elements, positions, rings);
let l_cols = build_projection_matrix(&decomp, positions, elements);
let n_reduced = l_cols.len();
if n_reduced == 0 {
return MbhResult {
frequencies: vec![],
n_blocks: 0,
n_flexible: 0,
n_dof_reduced: 0,
n_dof_full: 3 * n_atoms,
speedup: 1.0,
};
}
let n_full = 3 * n_atoms;
let h = config.fd_step;
let ref_coords: Vec<f64> = positions.iter().flat_map(|p| p.iter().cloned()).collect();
let mut h_mbh = vec![vec![0.0; n_reduced]; n_reduced];
for i in 0..n_reduced {
let mut coords_p: Vec<f64> = ref_coords.clone();
let mut coords_m: Vec<f64> = ref_coords.clone();
for k in 0..n_full {
coords_p[k] += h * l_cols[i][k];
coords_m[k] -= h * l_cols[i][k];
}
let e_p = energy_fn(&coords_p);
let e_m = energy_fn(&coords_m);
let e_0 = energy_fn(&ref_coords);
h_mbh[i][i] = (e_p - 2.0 * e_0 + e_m) / (h * h);
for j in (i + 1)..n_reduced {
let mut cpp = ref_coords.clone();
let mut cpm = ref_coords.clone();
let mut cmp = ref_coords.clone();
let mut cmm = ref_coords.clone();
for k in 0..n_full {
cpp[k] += h * l_cols[i][k] + h * l_cols[j][k];
cpm[k] += h * l_cols[i][k] - h * l_cols[j][k];
cmp[k] -= h * l_cols[i][k] - h * l_cols[j][k];
cmm[k] -= h * l_cols[i][k] + h * l_cols[j][k];
}
let epp = energy_fn(&cpp);
let epm = energy_fn(&cpm);
let emp = energy_fn(&cmp);
let emm = energy_fn(&cmm);
h_mbh[i][j] = (epp - epm - emp + emm) / (4.0 * h * h);
h_mbh[j][i] = h_mbh[i][j];
}
}
let mut mass_diag = vec![0.0; n_full];
for i in 0..n_atoms {
let m = atomic_mass(elements[i]);
mass_diag[i * 3] = m;
mass_diag[i * 3 + 1] = m;
mass_diag[i * 3 + 2] = m;
}
let mut m_red = vec![vec![0.0; n_reduced]; n_reduced];
for i in 0..n_reduced {
for j in i..n_reduced {
let mut val = 0.0;
for k in 0..n_full {
val += l_cols[i][k] * mass_diag[k] * l_cols[j][k];
}
m_red[i][j] = val;
m_red[j][i] = val;
}
}
let m_mat = nalgebra::DMatrix::from_fn(n_reduced, n_reduced, |i, j| m_red[i][j]);
let h_mat = nalgebra::DMatrix::from_fn(n_reduced, n_reduced, |i, j| h_mbh[i][j]);
let m_eigen = m_mat.symmetric_eigen();
let mut m_inv_sqrt = nalgebra::DMatrix::zeros(n_reduced, n_reduced);
for i in 0..n_reduced {
if m_eigen.eigenvalues[i] > 1e-10 {
let val = 1.0 / m_eigen.eigenvalues[i].sqrt();
for r in 0..n_reduced {
for c in 0..n_reduced {
m_inv_sqrt[(r, c)] +=
val * m_eigen.eigenvectors[(r, i)] * m_eigen.eigenvectors[(c, i)];
}
}
}
}
let h_standard = &m_inv_sqrt * &h_mat * &m_inv_sqrt;
let eigen = h_standard.symmetric_eigen();
let conversion = 108.59;
let mut frequencies: Vec<f64> = eigen
.eigenvalues
.iter()
.map(|&e| {
if e >= 0.0 {
conversion * e.sqrt()
} else {
-conversion * (-e).sqrt() }
})
.collect();
frequencies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let speedup = if decomp.n_dof_reduced > 0 {
decomp.n_dof_full as f64 / decomp.n_dof_reduced as f64
} else {
1.0
};
MbhResult {
frequencies,
n_blocks: decomp.blocks.len(),
n_flexible: decomp.flexible_atoms.len(),
n_dof_reduced: decomp.n_dof_reduced,
n_dof_full: decomp.n_dof_full,
speedup,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn harmonic_energy(coords: &[f64]) -> f64 {
let k = 100.0; let r0 = 1.4;
let n = coords.len() / 3;
let mut e = 0.0;
for i in 0..n {
for j in (i + 1)..n {
let dx = coords[i * 3] - coords[j * 3];
let dy = coords[i * 3 + 1] - coords[j * 3 + 1];
let dz = coords[i * 3 + 2] - coords[j * 3 + 2];
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if r < 3.0 {
e += 0.5 * k * (r - r0).powi(2);
}
}
}
e
}
#[test]
fn test_mbh_no_blocks() {
let elements = vec![6, 1, 1];
let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
let rings: Vec<(Vec<usize>, bool)> = vec![];
let config = MbhConfig::default();
let result =
compute_mbh_frequencies(&elements, &positions, &rings, &harmonic_energy, &config);
assert_eq!(result.n_blocks, 0);
assert_eq!(result.n_flexible, 3);
assert_eq!(result.n_dof_reduced, 9);
assert_eq!(result.frequencies.len(), 9);
}
#[test]
fn test_mbh_with_ring() {
let _n = 9; let elements = vec![6u8; 9];
let positions: Vec<[f64; 3]> = (0..9)
.map(|i| {
if i < 6 {
let a = i as f64 * std::f64::consts::PI / 3.0;
[1.4 * a.cos(), 1.4 * a.sin(), 0.0]
} else {
[4.0 + (i - 6) as f64 * 1.5, 0.0, 0.0]
}
})
.collect();
let rings = vec![(vec![0, 1, 2, 3, 4, 5], true)];
let result = compute_mbh_frequencies(
&elements,
&positions,
&rings,
&harmonic_energy,
&MbhConfig::default(),
);
assert_eq!(result.n_blocks, 1);
assert_eq!(result.n_flexible, 3);
assert_eq!(result.n_dof_reduced, 15);
assert_eq!(result.n_dof_full, 27);
assert!(
result.speedup > 1.0,
"Should show speedup: {}",
result.speedup
);
}
#[test]
fn test_mbh_frequencies_finite() {
let elements = vec![6u8; 6];
let positions: Vec<[f64; 3]> = (0..6)
.map(|i| {
let a = i as f64 * std::f64::consts::PI / 3.0;
[1.4 * a.cos(), 1.4 * a.sin(), 0.0]
})
.collect();
let rings = vec![(vec![0, 1, 2, 3, 4, 5], true)];
let result = compute_mbh_frequencies(
&elements,
&positions,
&rings,
&harmonic_energy,
&MbhConfig::default(),
);
for &f in &result.frequencies {
assert!(f.is_finite(), "Frequency should be finite: {}", f);
}
}
}