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;
pub type Quat = [F; 4];
#[inline]
fn quat_norm(q: Quat) -> F {
(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt()
}
#[inline]
fn quat_dot(a: Quat, b: Quat) -> F {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
}
pub fn angular_distance(q1: Quat, q2: Quat, equivalent_orientations: bool) -> F {
let n1 = quat_norm(q1);
let n2 = quat_norm(q2);
if n1 == 0.0 || n2 == 0.0 {
return 0.0;
}
let mut d = quat_dot(q1, q2) / (n1 * n2);
if equivalent_orientations {
d = d.abs();
}
2.0 * d.clamp(-1.0, 1.0).acos()
}
#[derive(Debug, Clone)]
pub struct AngularSeparationGlobal {
equivalent_orientations: bool,
}
impl Default for AngularSeparationGlobal {
fn default() -> Self {
Self::new()
}
}
impl AngularSeparationGlobal {
pub fn new() -> Self {
Self {
equivalent_orientations: true,
}
}
pub fn with_equivalent_orientations(mut self, on: bool) -> Self {
self.equivalent_orientations = on;
self
}
}
pub struct AngularSeparationGlobalArgs<'a> {
pub query: &'a [Quat],
pub global: &'a [Quat],
}
impl Compute for AngularSeparationGlobal {
type Args<'a> = AngularSeparationGlobalArgs<'a>;
type Output = Vec<AngularSeparationGlobalResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
args: AngularSeparationGlobalArgs<'a>,
) -> Result<Vec<AngularSeparationGlobalResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
if args.query.is_empty() || args.global.is_empty() {
return Err(ComputeError::EmptyInput);
}
let n_query = args.query.len();
let n_global = args.global.len();
let mut out = Vec::with_capacity(frames.len());
for _ in frames {
let mut a = Array2::<F>::zeros((n_query, n_global));
for i in 0..n_query {
for j in 0..n_global {
a[[i, j]] = angular_distance(
args.query[i],
args.global[j],
self.equivalent_orientations,
);
}
}
out.push(AngularSeparationGlobalResult { angles: a });
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct AngularSeparationNeighbor {
equivalent_orientations: bool,
}
impl Default for AngularSeparationNeighbor {
fn default() -> Self {
Self::new()
}
}
impl AngularSeparationNeighbor {
pub fn new() -> Self {
Self {
equivalent_orientations: true,
}
}
pub fn with_equivalent_orientations(mut self, on: bool) -> Self {
self.equivalent_orientations = on;
self
}
}
pub struct AngularSeparationNeighborArgs<'a> {
pub nlists: &'a [NeighborList],
pub query_orientations: &'a [Vec<Quat>],
pub point_orientations: &'a [Vec<Quat>],
}
impl Compute for AngularSeparationNeighbor {
type Args<'a> = AngularSeparationNeighborArgs<'a>;
type Output = Vec<AngularSeparationNeighborResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
args: AngularSeparationNeighborArgs<'a>,
) -> Result<Vec<AngularSeparationNeighborResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
let nf = frames.len();
if args.nlists.len() != nf
|| args.query_orientations.len() != nf
|| args.point_orientations.len() != nf
{
return Err(ComputeError::DimensionMismatch {
expected: nf,
got: args
.nlists
.len()
.min(args.query_orientations.len())
.min(args.point_orientations.len()),
what: "AngularSeparationNeighbor frame-aligned inputs",
});
}
let mut out = Vec::with_capacity(nf);
for k in 0..nf {
let nl = &args.nlists[k];
let q = &args.query_orientations[k];
let p = &args.point_orientations[k];
let i_idx = nl.query_point_indices();
let j_idx = nl.point_indices();
let mut angles = Vec::with_capacity(nl.n_pairs());
for pk in 0..nl.n_pairs() {
let i = i_idx[pk] as usize;
let j = j_idx[pk] as usize;
if i >= q.len() || j >= p.len() {
return Err(ComputeError::DimensionMismatch {
expected: i.max(j) + 1,
got: q.len().min(p.len()),
what: "AngularSeparationNeighbor orientations length",
});
}
angles.push(angular_distance(q[i], p[j], self.equivalent_orientations));
}
out.push(AngularSeparationNeighborResult { angles });
}
Ok(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct AngularSeparationGlobalResult {
pub angles: Array2<F>,
}
impl ComputeResult for AngularSeparationGlobalResult {}
#[derive(Debug, Clone, Default)]
pub struct AngularSeparationNeighborResult {
pub angles: Vec<F>,
}
impl ComputeResult for AngularSeparationNeighborResult {}
#[cfg(test)]
mod tests {
use super::*;
use molrs::Frame;
const TOL: F = 1e-10;
fn frame() -> Frame {
Frame::new()
}
#[test]
fn identity_quaternion_zero_distance() {
let q = [1.0_f64, 0.0, 0.0, 0.0];
assert!(angular_distance(q, q, true).abs() < TOL);
assert!(angular_distance(q, q, false).abs() < TOL);
}
#[test]
fn antipodal_quaternions_zero_with_equivalence() {
let q = [1.0_f64, 0.0, 0.0, 0.0];
let neg_q = [-1.0_f64, 0.0, 0.0, 0.0];
assert!(angular_distance(q, neg_q, true).abs() < TOL);
let theta = angular_distance(q, neg_q, false);
assert!((theta - 2.0 * std::f64::consts::PI).abs() < TOL);
}
#[test]
fn ninety_degree_rotation_about_z() {
let q = [
std::f64::consts::FRAC_PI_4.cos(),
0.0,
0.0,
std::f64::consts::FRAC_PI_4.sin(),
];
let identity = [1.0_f64, 0.0, 0.0, 0.0];
let theta = angular_distance(q, identity, true);
assert!((theta - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
}
#[test]
fn global_shape_and_values() {
let q = vec![[1.0_f64, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]];
let g = vec![[1.0_f64, 0.0, 0.0, 0.0]];
let r = &AngularSeparationGlobal::new()
.compute(
&[&frame()],
AngularSeparationGlobalArgs {
query: &q,
global: &g,
},
)
.unwrap()[0];
assert_eq!(r.angles.dim(), (2, 1));
assert!(r.angles[[0, 0]].abs() < TOL);
assert!((r.angles[[1, 0]] - std::f64::consts::PI).abs() < 1e-12);
}
#[test]
fn empty_inputs_error() {
let err = AngularSeparationGlobal::new()
.compute(
&[&frame()],
AngularSeparationGlobalArgs {
query: &[],
global: &[[1.0, 0.0, 0.0, 0.0]],
},
)
.unwrap_err();
assert!(matches!(err, ComputeError::EmptyInput));
}
}