use crate::compute::result::ComputeResult;
use molrs::spatial::neighbors::NeighborList;
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use ndarray::Array2;
use crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
use crate::compute::util::get_positions_ref;
const PI: F = std::f64::consts::PI;
const TWO_PI: F = 2.0 * PI;
#[derive(Debug, Clone, Copy)]
pub struct BondOrder {
n_theta: usize,
n_phi: usize,
}
impl BondOrder {
pub fn new(n_theta: usize, n_phi: usize) -> Result<Self, ComputeError> {
if n_theta == 0 || n_phi == 0 {
return Err(ComputeError::OutOfRange {
field: "BondOrder bin counts",
value: format!("({n_theta}, {n_phi})"),
});
}
Ok(Self { n_theta, n_phi })
}
pub fn n_theta(&self) -> usize {
self.n_theta
}
pub fn n_phi(&self) -> usize {
self.n_phi
}
fn one_frame<FA: FrameAccess>(
&self,
frame: &FA,
nlist: &NeighborList,
) -> Result<BondOrderResult, ComputeError> {
let (xs_p, _, _) = get_positions_ref(frame)?;
let _ = xs_p;
let d_theta = PI / self.n_theta as F;
let d_phi = TWO_PI / self.n_phi as F;
let theta_edges: Vec<F> = (0..=self.n_theta).map(|i| i as F * d_theta).collect();
let phi_edges: Vec<F> = (0..=self.n_phi).map(|i| -PI + i as F * d_phi).collect();
let mut counts = Array2::<u64>::zeros((self.n_theta, self.n_phi));
let vectors = nlist.vectors();
let n_pairs = nlist.n_pairs();
let symmetric = matches!(
nlist.mode(),
molrs::spatial::neighbors::QueryMode::SelfQuery
);
for k in 0..n_pairs {
let dx = vectors[[k, 0]];
let dy = vectors[[k, 1]];
let dz = vectors[[k, 2]];
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if r == 0.0 {
continue;
}
push_angle(&mut counts, dx, dy, dz, r, self.n_theta, self.n_phi);
if symmetric {
push_angle(&mut counts, -dx, -dy, -dz, r, self.n_theta, self.n_phi);
}
}
let mut bond_order = Array2::<F>::zeros((self.n_theta, self.n_phi));
for i in 0..self.n_theta {
let theta_c = (i as F + 0.5) * d_theta;
let solid = theta_c.sin() * d_theta * d_phi;
if solid <= 0.0 {
continue;
}
for j in 0..self.n_phi {
bond_order[[i, j]] = counts[[i, j]] as F / solid;
}
}
Ok(BondOrderResult {
bond_order,
raw_counts: counts,
theta_edges,
phi_edges,
})
}
}
#[inline]
fn push_angle(counts: &mut Array2<u64>, dx: F, dy: F, dz: F, r: F, n_theta: usize, n_phi: usize) {
let theta = (dz / r).clamp(-1.0, 1.0).acos(); let phi = dy.atan2(dx); let it = ((theta / PI) * n_theta as F) as usize;
let it = it.min(n_theta - 1);
let ip = (((phi + PI) / TWO_PI) * n_phi as F) as usize;
let ip = ip.min(n_phi - 1);
counts[[it, ip]] += 1;
}
impl Compute for BondOrder {
type Args<'a> = &'a Vec<NeighborList>;
type Output = Vec<BondOrderResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
nlists: &'a Vec<NeighborList>,
) -> Result<Vec<BondOrderResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
if frames.len() != nlists.len() {
return Err(ComputeError::DimensionMismatch {
expected: frames.len(),
got: nlists.len(),
what: "neighbor-list count",
});
}
#[cfg(feature = "rayon")]
const PAR_THRESHOLD: usize = 2;
#[cfg(feature = "rayon")]
if frames.len() >= PAR_THRESHOLD {
use rayon::prelude::*;
return frames
.par_iter()
.zip(nlists.par_iter())
.map(|(f, nl)| self.one_frame(*f, nl))
.collect();
}
let mut out = Vec::with_capacity(frames.len());
for (f, nl) in frames.iter().zip(nlists.iter()) {
out.push(self.one_frame(*f, nl)?);
}
Ok(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct BondOrderResult {
pub bond_order: Array2<F>,
pub raw_counts: Array2<u64>,
pub theta_edges: Vec<F>,
pub phi_edges: Vec<F>,
}
impl ComputeResult for BondOrderResult {}
#[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 single_bond_lands_in_correct_bin() {
let frame = frame_with(&[[5.0, 5.0, 5.0], [5.0, 5.0, 6.0]], 20.0);
let nl = build_nlist(&frame, 1.5);
let r = &BondOrder::new(10, 10)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap()[0];
let total: u64 = r.raw_counts.iter().copied().sum();
assert_eq!(total, 2);
let top: u64 = (0..10).map(|j| r.raw_counts[[0, j]]).sum();
let bot: u64 = (0..10).map(|j| r.raw_counts[[9, j]]).sum();
assert_eq!(top, 1);
assert_eq!(bot, 1);
}
#[test]
fn octahedral_bonds_distribute_across_axes() {
let frame = frame_with(
&[
[5.0, 5.0, 5.0],
[6.0, 5.0, 5.0],
[4.0, 5.0, 5.0],
[5.0, 6.0, 5.0],
[5.0, 4.0, 5.0],
[5.0, 5.0, 6.0],
[5.0, 5.0, 4.0],
],
20.0,
);
let nl = build_nlist(&frame, 1.2);
let r = &BondOrder::new(8, 8)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap()[0];
let total: u64 = r.raw_counts.iter().copied().sum();
assert_eq!(total, 12);
}
#[test]
fn empty_input_error() {
let frames: Vec<&Frame> = Vec::new();
let err = BondOrder::new(10, 10)
.unwrap()
.compute(&frames, &Vec::<NeighborList>::new())
.unwrap_err();
assert!(matches!(err, ComputeError::EmptyInput));
}
#[test]
fn invalid_bins_error() {
assert!(BondOrder::new(0, 10).is_err());
assert!(BondOrder::new(10, 0).is_err());
}
}