use crate::compute::result::ComputeResult;
use molrs::spatial::neighbors::NeighborList;
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use ndarray::Array1;
use crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
#[derive(Debug, Clone)]
pub struct CorrelationFunction {
n_bins: usize,
r_min: F,
r_max: F,
bin_width: F,
bin_edges: Array1<F>,
bin_centers: Array1<F>,
}
impl CorrelationFunction {
pub fn new(n_bins: usize, r_max: F, r_min: F) -> Result<Self, ComputeError> {
if n_bins == 0 {
return Err(ComputeError::OutOfRange {
field: "CorrelationFunction::n_bins",
value: "0".into(),
});
}
if r_min.is_nan() || r_min < 0.0 {
return Err(ComputeError::OutOfRange {
field: "CorrelationFunction::r_min",
value: r_min.to_string(),
});
}
if r_max.is_nan() || r_max <= r_min {
return Err(ComputeError::OutOfRange {
field: "CorrelationFunction::r_max",
value: format!("r_max={r_max}, r_min={r_min}"),
});
}
let bin_width = (r_max - r_min) / n_bins as F;
let bin_edges = Array1::from_iter((0..=n_bins).map(|i| r_min + i as F * bin_width));
let bin_centers =
Array1::from_iter((0..n_bins).map(|i| r_min + (i as F + 0.5) * bin_width));
Ok(Self {
n_bins,
r_min,
r_max,
bin_width,
bin_edges,
bin_centers,
})
}
pub fn n_bins(&self) -> usize {
self.n_bins
}
pub fn bin_edges(&self) -> &Array1<F> {
&self.bin_edges
}
pub fn bin_centers(&self) -> &Array1<F> {
&self.bin_centers
}
fn one_frame(
&self,
nlist: &NeighborList,
values_a: &[F],
values_b: &[F],
) -> Result<CorrelationFunctionResult, ComputeError> {
let r_min_sq = self.r_min * self.r_min;
let r_max_sq = self.r_max * self.r_max;
let i_idx = nlist.query_point_indices();
let j_idx = nlist.point_indices();
let dist_sq = nlist.dist_sq();
let n_pairs = nlist.n_pairs();
let symmetric = matches!(
nlist.mode(),
molrs::spatial::neighbors::QueryMode::SelfQuery
);
let mut sum = Array1::<F>::zeros(self.n_bins);
let mut counts = Array1::<u64>::zeros(self.n_bins);
for k in 0..n_pairs {
let d2 = dist_sq[k];
if d2 < r_min_sq || d2 >= r_max_sq || d2 == 0.0 {
continue;
}
let d = d2.sqrt();
let b = ((d - self.r_min) / self.bin_width) as usize;
if b >= self.n_bins {
continue;
}
let i = i_idx[k] as usize;
let j = j_idx[k] as usize;
if i >= values_a.len() || j >= values_b.len() {
return Err(ComputeError::DimensionMismatch {
expected: i.max(j) + 1,
got: values_a.len().min(values_b.len()),
what: "CorrelationFunction values length",
});
}
sum[b] += values_a[i] * values_b[j];
counts[b] += 1;
if symmetric {
sum[b] += values_a[j] * values_b[i];
counts[b] += 1;
}
}
let mut correlation = Array1::<F>::zeros(self.n_bins);
for b in 0..self.n_bins {
if counts[b] > 0 {
correlation[b] = sum[b] / counts[b] as F;
}
}
Ok(CorrelationFunctionResult {
bin_edges: self.bin_edges.clone(),
bin_centers: self.bin_centers.clone(),
bin_counts: counts,
correlation,
})
}
}
pub struct CorrelationArgs<'a> {
pub nlists: &'a [NeighborList],
pub values_a: &'a [Vec<F>],
pub values_b: &'a [Vec<F>],
}
impl Compute for CorrelationFunction {
type Args<'a> = CorrelationArgs<'a>;
type Output = Vec<CorrelationFunctionResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
args: CorrelationArgs<'a>,
) -> Result<Vec<CorrelationFunctionResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
let nf = frames.len();
if args.nlists.len() != nf || args.values_a.len() != nf || args.values_b.len() != nf {
return Err(ComputeError::DimensionMismatch {
expected: nf,
got: args
.nlists
.len()
.min(args.values_a.len())
.min(args.values_b.len()),
what: "CorrelationFunction frame-aligned inputs",
});
}
#[cfg(feature = "rayon")]
const PAR_THRESHOLD: usize = 2;
#[cfg(feature = "rayon")]
if nf >= PAR_THRESHOLD {
use rayon::prelude::*;
return (0..nf)
.into_par_iter()
.map(|k| self.one_frame(&args.nlists[k], &args.values_a[k], &args.values_b[k]))
.collect();
}
let mut out = Vec::with_capacity(nf);
for k in 0..nf {
out.push(self.one_frame(&args.nlists[k], &args.values_a[k], &args.values_b[k])?);
}
Ok(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct CorrelationFunctionResult {
pub bin_edges: Array1<F>,
pub bin_centers: Array1<F>,
pub bin_counts: Array1<u64>,
pub correlation: Array1<F>,
}
impl ComputeResult for CorrelationFunctionResult {}
#[cfg(test)]
mod tests {
use super::*;
use crate::compute::test_support::nlist_from_frame;
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) -> 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], [false; 3]).unwrap());
frame
}
fn build_nlist(frame: &Frame, cutoff: F) -> NeighborList {
nlist_from_frame(frame, cutoff)
}
#[test]
fn constant_values_yield_constant_correlation() {
let positions = [
[1.0_f64, 1.0, 1.0],
[2.0, 1.0, 1.0],
[3.0, 1.0, 1.0],
[4.0, 1.0, 1.0],
];
let frame = frame_with(&positions, 20.0);
let nl = build_nlist(&frame, 5.0);
let vals = vec![1.0_f64; positions.len()];
let cf = CorrelationFunction::new(10, 5.0, 0.0).unwrap();
let r = &cf
.compute(
&[&frame],
CorrelationArgs {
nlists: &[nl],
values_a: std::slice::from_ref(&vals),
values_b: std::slice::from_ref(&vals),
},
)
.unwrap()[0];
for b in 0..10 {
if r.bin_counts[b] > 0 {
assert!(
(r.correlation[b] - 1.0).abs() < 1e-12,
"bin {b}: got {}",
r.correlation[b]
);
}
}
}
#[test]
fn alternating_values_average_to_minus_one() {
let positions = [
[0.0_f64, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
];
let frame = frame_with(&positions, 20.0);
let nl = build_nlist(&frame, 1.2);
let vals = vec![1.0_f64, -1.0, 1.0, -1.0];
let cf = CorrelationFunction::new(5, 1.5, 0.0).unwrap();
let r = &cf
.compute(
&[&frame],
CorrelationArgs {
nlists: &[nl],
values_a: std::slice::from_ref(&vals),
values_b: std::slice::from_ref(&vals),
},
)
.unwrap()[0];
let first = r.bin_counts.iter().position(|&c| c > 0).unwrap();
assert!((r.correlation[first] + 1.0).abs() < 1e-12);
}
#[test]
fn empty_bin_returns_zero() {
let frame = frame_with(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], 20.0);
let nl = build_nlist(&frame, 1.5);
let vals = vec![1.0_f64, 1.0];
let cf = CorrelationFunction::new(10, 5.0, 0.0).unwrap();
let r = &cf
.compute(
&[&frame],
CorrelationArgs {
nlists: &[nl],
values_a: std::slice::from_ref(&vals),
values_b: std::slice::from_ref(&vals),
},
)
.unwrap()[0];
for b in 4..10 {
assert_eq!(r.bin_counts[b], 0);
assert_eq!(r.correlation[b], 0.0);
}
}
#[test]
fn invalid_params_error() {
assert!(CorrelationFunction::new(0, 1.0, 0.0).is_err());
assert!(CorrelationFunction::new(10, 1.0, 1.0).is_err());
assert!(CorrelationFunction::new(10, 0.5, 1.0).is_err());
}
#[test]
fn parallel_matches_serial() {
let positions = [
[1.0_f64, 1.0, 1.0],
[2.0, 1.0, 1.0],
[3.0, 1.0, 1.0],
[4.0, 1.0, 1.0],
];
let frame = frame_with(&positions, 20.0);
let nl = build_nlist(&frame, 5.0);
let vals = vec![1.0_f64; positions.len()];
let cf = CorrelationFunction::new(10, 5.0, 0.0).unwrap();
let solo = cf
.compute(
&[&frame],
CorrelationArgs {
nlists: std::slice::from_ref(&nl),
values_a: std::slice::from_ref(&vals),
values_b: std::slice::from_ref(&vals),
},
)
.unwrap();
let nls = vec![nl.clone(), nl.clone()];
let vs = vec![vals.clone(), vals.clone()];
let par = cf
.compute(
&[&frame, &frame],
CorrelationArgs {
nlists: &nls,
values_a: &vs,
values_b: &vs,
},
)
.unwrap();
assert_eq!(par.len(), 2);
assert_eq!(par[0].correlation, solo[0].correlation);
assert_eq!(par[0].bin_counts, solo[0].bin_counts);
assert_eq!(par[1].correlation, solo[0].correlation);
}
}