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;
#[derive(Debug, Clone, Default)]
pub struct LocalBondProjection {
use_orientations: bool,
}
impl LocalBondProjection {
pub fn new() -> Self {
Self::default()
}
pub fn with_query_orientations(mut self, on: bool) -> Self {
self.use_orientations = on;
self
}
}
pub struct LocalBondProjectionArgs<'a> {
pub nlists: &'a [NeighborList],
pub proj_vectors: &'a [[F; 3]],
pub query_orientations: Option<&'a [Vec<[F; 4]>]>,
}
#[inline]
fn rotate_by_quat(q: [F; 4], v: [F; 3]) -> [F; 3] {
let (w, x, y, z) = (q[0], q[1], q[2], q[3]);
let tx = 2.0 * (y * v[2] - z * v[1]);
let ty = 2.0 * (z * v[0] - x * v[2]);
let tz = 2.0 * (x * v[1] - y * v[0]);
[
v[0] + w * tx + (y * tz - z * ty),
v[1] + w * ty + (z * tx - x * tz),
v[2] + w * tz + (x * ty - y * tx),
]
}
impl Compute for LocalBondProjection {
type Args<'a> = LocalBondProjectionArgs<'a>;
type Output = Vec<LocalBondProjectionResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
args: LocalBondProjectionArgs<'a>,
) -> Result<Vec<LocalBondProjectionResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
if args.nlists.len() != frames.len() {
return Err(ComputeError::DimensionMismatch {
expected: frames.len(),
got: args.nlists.len(),
what: "neighbor-list count",
});
}
if args.proj_vectors.is_empty() {
return Err(ComputeError::EmptyInput);
}
if self.use_orientations {
match args.query_orientations {
Some(q) if q.len() == frames.len() => {}
_ => {
return Err(ComputeError::DimensionMismatch {
expected: frames.len(),
got: args.query_orientations.map(|q| q.len()).unwrap_or(0),
what: "query_orientations frame count",
});
}
}
}
let n_proj = args.proj_vectors.len();
let refs: Vec<[F; 3]> = args
.proj_vectors
.iter()
.map(|v| {
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if n == 0.0 {
[0.0, 0.0, 0.0]
} else {
[v[0] / n, v[1] / n, v[2] / n]
}
})
.collect();
let mut out = Vec::with_capacity(frames.len());
for (k, nl) in args.nlists.iter().enumerate() {
let vectors = nl.vectors();
let i_idx = nl.query_point_indices();
let n_pairs = nl.n_pairs();
let mut p = Array2::<F>::zeros((n_pairs, n_proj));
for pk in 0..n_pairs {
let dx = vectors[[pk, 0]];
let dy = vectors[[pk, 1]];
let dz = vectors[[pk, 2]];
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if r == 0.0 {
continue;
}
let bond = [dx / r, dy / r, dz / r];
let qrot = if self.use_orientations {
Some(args.query_orientations.unwrap()[k][i_idx[pk] as usize])
} else {
None
};
for (j, ref_v) in refs.iter().enumerate() {
let dir = match qrot {
None => *ref_v,
Some(q) => rotate_by_quat(q, *ref_v),
};
let dot = bond[0] * dir[0] + bond[1] * dir[1] + bond[2] * dir[2];
p[[pk, j]] = dot;
}
}
out.push(LocalBondProjectionResult { projections: p });
}
Ok(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct LocalBondProjectionResult {
pub projections: Array2<F>,
}
impl ComputeResult for LocalBondProjectionResult {}
#[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};
const TOL: F = 1e-12;
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 bond_along_x_projects_to_one_on_x_axis() {
let frame = frame_with(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], 10.0);
let nl = build_nlist(&frame, 1.5);
let r = &LocalBondProjection::new()
.compute(
&[&frame],
LocalBondProjectionArgs {
nlists: std::slice::from_ref(&nl),
proj_vectors: &[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
query_orientations: None,
},
)
.unwrap()[0];
assert_eq!(r.projections.dim(), (1, 2));
assert!((r.projections[[0, 0]] - 1.0).abs() < TOL);
assert!((r.projections[[0, 1]]).abs() < TOL);
}
#[test]
fn rotation_changes_projection() {
let frame = frame_with(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], 10.0);
let nl = build_nlist(&frame, 1.5);
let q = [
std::f64::consts::FRAC_PI_4.cos(),
0.0,
0.0,
std::f64::consts::FRAC_PI_4.sin(),
];
let r = &LocalBondProjection::new()
.with_query_orientations(true)
.compute(
&[&frame],
LocalBondProjectionArgs {
nlists: std::slice::from_ref(&nl),
proj_vectors: &[[1.0, 0.0, 0.0]],
query_orientations: Some(&[vec![q, q]]),
},
)
.unwrap()[0];
assert!(r.projections[[0, 0]].abs() < 1e-12);
}
#[test]
fn empty_proj_vectors_error() {
let frame = frame_with(&[[0.0, 0.0, 0.0]], 10.0);
let nl = build_nlist(&frame, 1.0);
let err = LocalBondProjection::new()
.compute(
&[&frame],
LocalBondProjectionArgs {
nlists: std::slice::from_ref(&nl),
proj_vectors: &[],
query_orientations: None,
},
)
.unwrap_err();
assert!(matches!(err, ComputeError::EmptyInput));
}
}