#![allow(clippy::needless_range_loop)]
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use super::ClusterResult;
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
use crate::compute::traits::Compute;
use crate::compute::util::{MicHelper, get_positions_ref};
#[derive(Debug, Clone, Default)]
pub struct ClusterPropertiesResult {
pub sizes: Vec<usize>,
pub centers: Vec<[F; 3]>,
pub centers_of_mass: Vec<[F; 3]>,
pub cluster_masses: Vec<F>,
pub gyration_tensors: Vec<[[F; 3]; 3]>,
pub radii_of_gyration: Vec<F>,
}
impl ComputeResult for ClusterPropertiesResult {}
#[derive(Debug, Clone, Default)]
pub struct ClusterProperties {
masses: Option<Vec<F>>,
}
impl ClusterProperties {
pub fn new() -> Self {
Self { masses: None }
}
pub fn with_masses(self, masses: &[F]) -> Self {
Self {
masses: Some(masses.to_vec()),
}
}
fn one_frame<FA: FrameAccess>(
&self,
frame: &FA,
clusters: &ClusterResult,
) -> Result<ClusterPropertiesResult, ComputeError> {
let (xs_p, ys_p, zs_p) = get_positions_ref(frame)?;
let xs = xs_p.slice();
let ys = ys_p.slice();
let zs = zs_p.slice();
let n = xs.len();
if let Some(ref ms) = self.masses
&& ms.len() != n
{
return Err(ComputeError::DimensionMismatch {
expected: n,
got: ms.len(),
what: "ClusterProperties::masses",
});
}
let mic = MicHelper::from_simbox(frame.simbox_ref());
let nc = clusters.num_clusters;
let masses_ref = self.masses.as_deref();
let mut ref_pos = vec![[0.0_f64; 3]; nc];
let mut has_ref = vec![false; nc];
let mut sum_d = vec![[0.0_f64; 3]; nc]; let mut sum_m_d = vec![[0.0_f64; 3]; nc]; let mut counts = vec![0_usize; nc];
let mut total_mass = vec![0.0_f64; nc];
for (i, &cid) in clusters.cluster_idx.iter().enumerate() {
if cid < 0 {
continue;
}
let c = cid as usize;
let pos = [xs[i], ys[i], zs[i]];
let m = masses_ref.map_or(1.0, |ms| ms[i]);
if !has_ref[c] {
ref_pos[c] = pos;
has_ref[c] = true;
}
let d = mic.disp(ref_pos[c], pos);
sum_d[c][0] += d[0];
sum_d[c][1] += d[1];
sum_d[c][2] += d[2];
sum_m_d[c][0] += m * d[0];
sum_m_d[c][1] += m * d[1];
sum_m_d[c][2] += m * d[2];
counts[c] += 1;
total_mass[c] += m;
}
let mut centers = vec![[0.0_f64; 3]; nc];
let mut centers_of_mass = vec![[0.0_f64; 3]; nc];
for c in 0..nc {
let count = counts[c] as F;
if count > 0.0 {
centers[c][0] = ref_pos[c][0] + sum_d[c][0] / count;
centers[c][1] = ref_pos[c][1] + sum_d[c][1] / count;
centers[c][2] = ref_pos[c][2] + sum_d[c][2] / count;
}
if total_mass[c] > 0.0 {
let m = total_mass[c];
centers_of_mass[c][0] = ref_pos[c][0] + sum_m_d[c][0] / m;
centers_of_mass[c][1] = ref_pos[c][1] + sum_m_d[c][1] / m;
centers_of_mass[c][2] = ref_pos[c][2] + sum_m_d[c][2] / m;
}
}
let mut gyration = vec![[[0.0_f64; 3]; 3]; nc];
for (i, &cid) in clusters.cluster_idx.iter().enumerate() {
if cid < 0 {
continue;
}
let c = cid as usize;
let pos = [xs[i], ys[i], zs[i]];
let m = masses_ref.map_or(1.0, |ms| ms[i]);
let d = mic.disp(centers_of_mass[c], pos);
for a in 0..3 {
for b in 0..3 {
gyration[c][a][b] += m * d[a] * d[b];
}
}
}
for c in 0..nc {
if total_mass[c] > 0.0 {
let inv = 1.0 / total_mass[c];
for a in 0..3 {
for b in 0..3 {
gyration[c][a][b] *= inv;
}
}
}
}
let radii_of_gyration: Vec<F> = (0..nc)
.map(|c| {
let tr = gyration[c][0][0] + gyration[c][1][1] + gyration[c][2][2];
tr.max(0.0).sqrt()
})
.collect();
Ok(ClusterPropertiesResult {
sizes: counts,
centers,
centers_of_mass,
cluster_masses: total_mass,
gyration_tensors: gyration,
radii_of_gyration,
})
}
}
impl Compute for ClusterProperties {
type Args<'a> = &'a Vec<ClusterResult>;
type Output = Vec<ClusterPropertiesResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
clusters: &'a Vec<ClusterResult>,
) -> Result<Vec<ClusterPropertiesResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
if clusters.len() != frames.len() {
return Err(ComputeError::DimensionMismatch {
expected: frames.len(),
got: clusters.len(),
what: "ClusterResult count",
});
}
#[cfg(feature = "rayon")]
const PAR_THRESHOLD: usize = 4;
#[cfg(feature = "rayon")]
if frames.len() >= PAR_THRESHOLD {
use rayon::prelude::*;
return frames
.par_iter()
.zip(clusters.par_iter())
.map(|(frame, cl)| self.one_frame(*frame, cl))
.collect();
}
let mut out = Vec::with_capacity(frames.len());
for (frame, cl) in frames.iter().zip(clusters.iter()) {
out.push(self.one_frame(*frame, cl)?);
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use molrs::Frame;
use molrs::spatial::region::simbox::SimBox;
use molrs::store::block::Block;
use ndarray::{Array1 as A1, array};
fn frame_with(positions: &[[F; 3]], box_len: F, pbc: [bool; 3]) -> Frame {
let x = A1::from_iter(positions.iter().map(|p| p[0]));
let y = A1::from_iter(positions.iter().map(|p| p[1]));
let z = A1::from_iter(positions.iter().map(|p| p[2]));
let mut block = Block::new();
block.insert("x", x.into_dyn()).unwrap();
block.insert("y", y.into_dyn()).unwrap();
block.insert("z", z.into_dyn()).unwrap();
let mut frame = Frame::new();
frame.insert("atoms", block);
frame.simbox =
Some(SimBox::cube(box_len, array![0.0 as F, 0.0 as F, 0.0 as F], pbc).unwrap());
frame
}
fn manual_clusters(idx: &[i64]) -> ClusterResult {
let nc = (*idx.iter().max().unwrap_or(&-1) + 1).max(0) as usize;
let mut sizes = vec![0usize; nc];
for &c in idx {
if c >= 0 {
sizes[c as usize] += 1;
}
}
ClusterResult {
cluster_idx: ndarray::Array1::from_vec(idx.to_vec()),
num_clusters: nc,
cluster_sizes: sizes,
cluster_keys: vec![],
}
}
fn single(frame: &Frame, cl: ClusterResult, cp: ClusterProperties) -> ClusterPropertiesResult {
let out = cp.compute(&[frame], &vec![cl]).unwrap();
out.into_iter().next().unwrap()
}
#[test]
fn two_particle_uniform_mass() {
let frame = frame_with(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], 10.0, [false; 3]);
let cl = manual_clusters(&[0, 0]);
let r = single(&frame, cl, ClusterProperties::new());
assert_eq!(r.sizes, vec![2]);
assert!((r.centers[0][0] - 1.0).abs() < 1e-10);
assert!((r.centers_of_mass[0][0] - 1.0).abs() < 1e-10);
assert!((r.cluster_masses[0] - 2.0).abs() < 1e-10);
assert!((r.gyration_tensors[0][0][0] - 1.0).abs() < 1e-10);
assert!((r.radii_of_gyration[0] - 1.0).abs() < 1e-10);
}
#[test]
fn weighted_center_shifts_to_heavy_atom() {
let frame = frame_with(&[[1.0, 0.0, 0.0], [3.0, 0.0, 0.0]], 10.0, [false; 3]);
let cl = manual_clusters(&[0, 0]);
let r = single(
&frame,
cl,
ClusterProperties::new().with_masses(&[3.0, 1.0]),
);
assert!((r.centers_of_mass[0][0] - 1.5).abs() < 1e-10);
assert!((r.centers[0][0] - 2.0).abs() < 1e-10);
}
#[test]
fn two_clusters_independent() {
let pos = [
[1.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
[5.0, 0.0, 0.0],
[7.0, 0.0, 0.0],
];
let frame = frame_with(&pos, 20.0, [false; 3]);
let cl = manual_clusters(&[0, 0, 1, 1]);
let r = single(&frame, cl, ClusterProperties::new());
assert_eq!(r.sizes, vec![2, 2]);
assert!((r.centers[0][0] - 1.0).abs() < 1e-10);
assert!(r.radii_of_gyration[0].abs() < 1e-10);
assert!((r.centers[1][0] - 6.0).abs() < 1e-10);
assert!((r.radii_of_gyration[1] - 1.0).abs() < 1e-10);
}
#[test]
fn filtered_atoms_ignored() {
let pos = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [100.0, 100.0, 100.0]];
let frame = frame_with(&pos, 200.0, [false; 3]);
let cl = manual_clusters(&[0, 0, -1]); let r = single(&frame, cl, ClusterProperties::new());
assert_eq!(r.sizes, vec![2]);
assert!((r.centers[0][0] - 1.0).abs() < 1e-10);
}
#[test]
fn pbc_wrapped_cluster() {
let pos = [[0.5, 5.0, 5.0], [9.5, 5.0, 5.0]];
let frame = frame_with(&pos, 10.0, [true, true, true]);
let cl = manual_clusters(&[0, 0]);
let r = single(&frame, cl, ClusterProperties::new());
assert!(
(r.gyration_tensors[0][0][0] - 0.25).abs() < 1e-9,
"PBC-aware G_xx = {} (expected 0.25)",
r.gyration_tensors[0][0][0]
);
assert!((r.radii_of_gyration[0] - 0.5).abs() < 1e-9);
}
#[test]
fn masses_dimension_mismatch_errors() {
let frame = frame_with(&[[0.0, 0.0, 0.0]; 2], 10.0, [false; 3]);
let cl = manual_clusters(&[0, 0]);
let err = ClusterProperties::new()
.with_masses(&[1.0])
.compute(&[&frame], &vec![cl])
.unwrap_err();
assert!(matches!(err, ComputeError::DimensionMismatch { .. }));
}
}