#![allow(clippy::needless_range_loop)]
use crate::compute::result::ComputeResult;
use molrs::math::complex::Complex;
use molrs::spatial::neighbors::NeighborList;
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use super::steinhardt::compute_qlm;
use crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
use crate::compute::util::get_positions_ref;
#[derive(Debug, Clone, Copy)]
pub struct SolidLiquid {
l: u32,
q_threshold: F,
n_threshold: u32,
normalize_q: bool,
}
impl SolidLiquid {
pub fn new(l: u32) -> Self {
Self {
l,
q_threshold: 0.7,
n_threshold: 6,
normalize_q: true,
}
}
pub fn with_q_threshold(mut self, t: F) -> Self {
self.q_threshold = t;
self
}
pub fn with_n_threshold(mut self, n: u32) -> Self {
self.n_threshold = n;
self
}
pub fn with_normalize_q(mut self, on: bool) -> Self {
self.normalize_q = on;
self
}
fn one_frame<FA: FrameAccess>(
&self,
frame: &FA,
nlist: &NeighborList,
) -> Result<SolidLiquidResult, ComputeError> {
let (xs_p, _, _) = get_positions_ref(frame)?;
let n = xs_p.slice().len();
let m_count = (2 * self.l + 1) as usize;
let qlm = compute_qlm(frame, nlist, self.l)?;
let mut norms = vec![0.0_f64; n];
for i in 0..n {
let off = i * m_count;
let mut s: F = 0.0;
for m in 0..m_count {
s += qlm[off + m].norm_sqr();
}
norms[i] = s.sqrt();
}
let i_idx = nlist.query_point_indices();
let j_idx = nlist.point_indices();
let n_pairs = nlist.n_pairs();
let mut n_solid_bonds = vec![0_u32; n];
for k in 0..n_pairs {
let i = i_idx[k] as usize;
let j = j_idx[k] as usize;
let mut dot = Complex::ZERO;
let off_i = i * m_count;
let off_j = j * m_count;
for m in 0..m_count {
dot += qlm[off_i + m].conj() * qlm[off_j + m];
}
let real = if self.normalize_q {
let denom = norms[i] * norms[j];
if denom > 0.0 { dot.re / denom } else { 0.0 }
} else {
dot.re
};
if real > self.q_threshold {
n_solid_bonds[i] += 1;
n_solid_bonds[j] += 1;
}
}
let is_solid: Vec<bool> = n_solid_bonds
.iter()
.map(|&c| c >= self.n_threshold)
.collect();
Ok(SolidLiquidResult {
l: self.l,
n_solid_bonds,
is_solid,
})
}
}
impl Compute for SolidLiquid {
type Args<'a> = &'a Vec<NeighborList>;
type Output = Vec<SolidLiquidResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
nlists: &'a Vec<NeighborList>,
) -> Result<Vec<SolidLiquidResult>, 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 SolidLiquidResult {
pub l: u32,
pub n_solid_bonds: Vec<u32>,
pub is_solid: Vec<bool>,
}
impl ComputeResult for SolidLiquidResult {}
#[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, pbc: [bool; 3]) -> 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], pbc).unwrap());
frame
}
fn build_nlist(frame: &Frame, cutoff: F) -> NeighborList {
nlist_from_frame(frame, cutoff)
}
fn paired_octahedra(box_len: F) -> Frame {
let mut p: Vec<[F; 3]> = Vec::new();
for &(cx, cy, cz) in &[(5.0_f64, 5.0, 5.0), (10.0_f64, 5.0, 5.0)] {
p.push([cx, cy, cz]);
for &(dx, dy, dz) in &[
(1.0_f64, 0.0, 0.0),
(-1.0_f64, 0.0, 0.0),
(0.0_f64, 1.0, 0.0),
(0.0_f64, -1.0, 0.0),
(0.0_f64, 0.0, 1.0),
(0.0_f64, 0.0, -1.0),
] {
p.push([cx + dx, cy + dy, cz + dz]);
}
}
frame_with(&p, box_len, [false; 3])
}
#[test]
fn dot_product_self_is_one() {
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,
[false; 3],
);
let nl = build_nlist(&frame, 1.2);
let r = &SolidLiquid::new(6)
.with_q_threshold(-2.0) .with_n_threshold(1)
.compute(&[&frame], &vec![nl])
.unwrap()[0];
assert_eq!(r.n_solid_bonds[0], 6);
for i in 1..7 {
assert_eq!(r.n_solid_bonds[i], 1);
}
}
#[test]
fn identical_environments_score_one() {
let frame = paired_octahedra(20.0);
let positions = [[5.0_f64, 5.0, 5.0], [10.0_f64, 5.0, 5.0]];
let centres = frame_with(
&[
positions[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],
positions[1],
[11.0, 5.0, 5.0],
[9.0, 5.0, 5.0],
[10.0, 6.0, 5.0],
[10.0, 4.0, 5.0],
[10.0, 5.0, 6.0],
[10.0, 5.0, 4.0],
],
20.0,
[false; 3],
);
let nl = build_nlist(¢res, 1.2);
let qlm = compute_qlm(¢res, &nl, 6).unwrap();
let m = 13_usize; for k in 0..m {
let a = qlm[k];
let b = qlm[7 * m + k];
assert!(
(a.re - b.re).abs() < 1e-12 && (a.im - b.im).abs() < 1e-12,
"centre qℓm components must match across symmetric octahedra"
);
}
let _ = frame; }
#[test]
fn deterministic_across_calls() {
let frame = paired_octahedra(20.0);
let nl = build_nlist(&frame, 1.2);
let sl = SolidLiquid::new(6);
let a = sl.compute(&[&frame], &vec![nl.clone()]).unwrap();
let b = sl.compute(&[&frame], &vec![nl]).unwrap();
assert_eq!(a[0].n_solid_bonds, b[0].n_solid_bonds);
assert_eq!(a[0].is_solid, b[0].is_solid);
}
#[test]
fn empty_frames_is_error() {
let frames: Vec<&Frame> = Vec::new();
let err = SolidLiquid::new(6)
.compute(&frames, &Vec::<NeighborList>::new())
.unwrap_err();
assert!(matches!(err, ComputeError::EmptyInput));
}
#[test]
fn high_threshold_makes_nothing_solid() {
let frame = paired_octahedra(20.0);
let nl = build_nlist(&frame, 1.2);
let r = &SolidLiquid::new(6)
.with_q_threshold(2.0) .compute(&[&frame], &vec![nl])
.unwrap()[0];
assert!(r.is_solid.iter().all(|&s| !s));
}
}