use crate::compute::result::ComputeResult;
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
#[derive(Debug, Clone, Copy)]
pub struct RotationalAutocorrelation {
l: u32,
}
impl RotationalAutocorrelation {
pub fn new(l: u32) -> Self {
Self { l }
}
pub fn l(&self) -> u32 {
self.l
}
}
type Quat = [F; 4];
#[inline]
fn quat_conj(q: Quat) -> Quat {
[q[0], -q[1], -q[2], -q[3]]
}
#[inline]
fn quat_mul(a: Quat, b: Quat) -> Quat {
[
a[0] * b[0] - a[1] * b[1] - a[2] * b[2] - a[3] * b[3],
a[0] * b[1] + a[1] * b[0] + a[2] * b[3] - a[3] * b[2],
a[0] * b[2] - a[1] * b[3] + a[2] * b[0] + a[3] * b[1],
a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + a[3] * b[0],
]
}
#[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 rotation_angle(q: Quat) -> F {
let n = quat_norm(q);
if n == 0.0 {
return 0.0;
}
2.0 * (q[0].abs() / n).clamp(0.0, 1.0).acos()
}
fn normalised_character(l: u32, theta: F) -> F {
let two_lp1 = 2.0 * l as F + 1.0;
if theta.abs() < 1e-12 {
return 1.0;
}
let num = ((l as F + 0.5) * theta).sin();
let den = (0.5 * theta).sin();
if den == 0.0 { 1.0 } else { num / den / two_lp1 }
}
pub struct RotationalAutocorrelationArgs<'a> {
pub ref_orientations: &'a [Quat],
pub orientations: &'a [Quat],
}
impl Compute for RotationalAutocorrelation {
type Args<'a> = RotationalAutocorrelationArgs<'a>;
type Output = Vec<RotationalAutocorrelationResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
args: RotationalAutocorrelationArgs<'a>,
) -> Result<Vec<RotationalAutocorrelationResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
if args.ref_orientations.len() != args.orientations.len() {
return Err(ComputeError::DimensionMismatch {
expected: args.ref_orientations.len(),
got: args.orientations.len(),
what: "RotationalAutocorrelation orientations",
});
}
let n = args.orientations.len();
let mut out = Vec::with_capacity(frames.len());
for _ in frames {
let mut psi = Vec::with_capacity(n);
let mut sum: F = 0.0;
for k in 0..n {
let q_rel = quat_mul(args.orientations[k], quat_conj(args.ref_orientations[k]));
let theta = rotation_angle(q_rel);
let v = normalised_character(self.l, theta);
psi.push(v);
sum += v;
}
let mean = if n > 0 { sum / n as F } else { 0.0 };
out.push(RotationalAutocorrelationResult {
l: self.l,
psi,
mean,
});
}
Ok(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct RotationalAutocorrelationResult {
pub l: u32,
pub psi: Vec<F>,
pub mean: F,
}
impl ComputeResult for RotationalAutocorrelationResult {}
#[cfg(test)]
mod tests {
use super::*;
use molrs::Frame;
fn frame() -> Frame {
Frame::new()
}
const TOL: F = 1e-12;
#[test]
fn unrotated_gives_psi_one() {
let q = [[1.0_f64, 0.0, 0.0, 0.0]; 5];
let r = &RotationalAutocorrelation::new(4)
.compute(
&[&frame()],
RotationalAutocorrelationArgs {
ref_orientations: &q,
orientations: &q,
},
)
.unwrap()[0];
for p in &r.psi {
assert!((p - 1.0).abs() < TOL);
}
assert!((r.mean - 1.0).abs() < TOL);
}
#[test]
fn antipodal_quaternion_is_same_rotation() {
let q = [1.0_f64, 0.0, 0.0, 0.0];
let neg_q = [-1.0_f64, 0.0, 0.0, 0.0];
let r = &RotationalAutocorrelation::new(2)
.compute(
&[&frame()],
RotationalAutocorrelationArgs {
ref_orientations: &[q],
orientations: &[neg_q],
},
)
.unwrap()[0];
assert!((r.psi[0] - 1.0).abs() < TOL);
}
#[test]
fn ninety_degree_rotation_known_character() {
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 r = &RotationalAutocorrelation::new(2)
.compute(
&[&frame()],
RotationalAutocorrelationArgs {
ref_orientations: &[identity],
orientations: &[q],
},
)
.unwrap()[0];
assert!((r.psi[0] - (-1.0 / 5.0)).abs() < 1e-12);
}
#[test]
fn mismatched_orientation_lengths_error() {
let err = RotationalAutocorrelation::new(2)
.compute(
&[&frame()],
RotationalAutocorrelationArgs {
ref_orientations: &[[1.0, 0.0, 0.0, 0.0]; 2],
orientations: &[[1.0, 0.0, 0.0, 0.0]; 3],
},
)
.unwrap_err();
assert!(matches!(err, ComputeError::DimensionMismatch { .. }));
}
}