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 crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
use crate::compute::util::get_positions_ref;
#[derive(Debug, Clone, Copy)]
pub struct Hexatic {
k: u32,
}
impl Hexatic {
pub fn new(k: u32) -> Result<Self, ComputeError> {
if k == 0 {
return Err(ComputeError::OutOfRange {
field: "Hexatic::k",
value: "0".into(),
});
}
Ok(Self { k })
}
pub fn k(&self) -> u32 {
self.k
}
fn one_frame<FA: FrameAccess>(
&self,
frame: &FA,
nlist: &NeighborList,
) -> Result<HexaticResult, ComputeError> {
let (xs_p, _, _) = get_positions_ref(frame)?;
let n = xs_p.slice().len();
let mut psi = vec![Complex::ZERO; n];
let mut count = vec![0_u32; n];
let i_idx = nlist.query_point_indices();
let j_idx = nlist.point_indices();
let vectors = nlist.vectors();
let n_pairs = nlist.n_pairs();
let parity = if self.k & 1 == 0 { 1.0 } else { -1.0 };
for kp in 0..n_pairs {
let i = i_idx[kp] as usize;
let j = j_idx[kp] as usize;
let dx = vectors[[kp, 0]];
let dy = vectors[[kp, 1]];
if dx == 0.0 && dy == 0.0 {
continue;
}
let theta = dy.atan2(dx);
let phase = Complex::from_polar(1.0, self.k as F * theta);
psi[i] += phase;
psi[j] += phase.scale(parity);
count[i] += 1;
count[j] += 1;
}
for i in 0..n {
if count[i] > 0 {
let inv = 1.0 / count[i] as F;
psi[i] = psi[i].scale(inv);
}
}
Ok(HexaticResult { k: self.k, psi })
}
}
impl Compute for Hexatic {
type Args<'a> = &'a Vec<NeighborList>;
type Output = Vec<HexaticResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
nlists: &'a Vec<NeighborList>,
) -> Result<Vec<HexaticResult>, 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 HexaticResult {
pub k: u32,
pub psi: Vec<Complex>,
}
impl ComputeResult for HexaticResult {}
#[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::ortho(
array![box_len, box_len, 1.0_f64],
array![0.0 as F, 0.0 as F, 0.0 as F],
[false, false, false],
)
.unwrap(),
);
frame
}
fn build_nlist(frame: &Frame, cutoff: F) -> NeighborList {
nlist_from_frame(frame, cutoff)
}
fn hex_environment(box_len: F) -> Frame {
let c = box_len * 0.5;
let mut positions = vec![[c, c, 0.0]];
for k in 0..6 {
let theta = 2.0_f64 * std::f64::consts::PI * k as F / 6.0;
positions.push([c + theta.cos(), c + theta.sin(), 0.0]);
}
frame_with(&positions, box_len)
}
fn square_environment(box_len: F) -> Frame {
let c = box_len * 0.5;
let positions = [
[c, c, 0.0],
[c + 1.0, c, 0.0],
[c - 1.0, c, 0.0],
[c, c + 1.0, 0.0],
[c, c - 1.0, 0.0],
];
frame_with(&positions, box_len)
}
#[test]
fn psi_6_on_perfect_hexagon_is_unity() {
let frame = hex_environment(20.0);
let nl = build_nlist(&frame, 1.2);
let res = Hexatic::new(6)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap();
let psi_center = res[0].psi[0];
assert!(
(psi_center.norm() - 1.0).abs() < 1e-10,
"|ψ_6(centre)| = {} (expected ≈ 1)",
psi_center.norm(),
);
}
#[test]
fn psi_4_on_perfect_square_is_unity() {
let frame = square_environment(20.0);
let nl = build_nlist(&frame, 1.2);
let res = Hexatic::new(4)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap();
let psi_center = res[0].psi[0];
assert!(
(psi_center.norm() - 1.0).abs() < 1e-10,
"|ψ_4(square)| = {} (expected ≈ 1)",
psi_center.norm(),
);
}
#[test]
fn psi_6_on_square_is_zero() {
let frame = square_environment(20.0);
let nl = build_nlist(&frame, 1.2);
let res = Hexatic::new(6)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap();
let psi_center = res[0].psi[0];
assert!(
psi_center.norm() < 1e-10,
"ψ_6(square) should be 0, got {}",
psi_center.norm(),
);
}
#[test]
fn isolated_particle_psi_is_zero() {
let frame = frame_with(&[[5.0, 5.0, 0.0]], 10.0);
let nl = build_nlist(&frame, 1.0);
let res = Hexatic::new(6)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap();
assert_eq!(res[0].psi[0], Complex::ZERO);
}
#[test]
fn k_zero_is_error() {
assert!(Hexatic::new(0).is_err());
}
#[test]
fn empty_frames_is_error() {
let frames: Vec<&Frame> = Vec::new();
let err = Hexatic::new(6)
.unwrap()
.compute(&frames, &Vec::<NeighborList>::new())
.unwrap_err();
assert!(matches!(err, ComputeError::EmptyInput));
}
#[test]
fn rotation_invariant_in_magnitude() {
let frame = hex_environment(20.0);
let nl = build_nlist(&frame, 1.2);
let a = Hexatic::new(6)
.unwrap()
.compute(&[&frame], &vec![nl])
.unwrap();
let mag = a[0].psi[0].norm();
let c = 10.0_f64;
let shift = 0.3_f64;
let mut positions = vec![[c, c, 0.0]];
for kk in 0..6 {
let theta = 2.0 * std::f64::consts::PI * kk as F / 6.0 + shift;
positions.push([c + theta.cos(), c + theta.sin(), 0.0]);
}
let frame2 = frame_with(&positions, 20.0);
let nl2 = build_nlist(&frame2, 1.2);
let b = Hexatic::new(6)
.unwrap()
.compute(&[&frame2], &vec![nl2])
.unwrap();
assert!(
(b[0].psi[0].norm() - mag).abs() < 1e-10,
"|ψ_6| should be rotation-invariant: {} vs {}",
mag,
b[0].psi[0].norm(),
);
}
#[test]
fn parallel_matches_serial() {
let frame = hex_environment(20.0);
let nl = build_nlist(&frame, 1.2);
let hx = Hexatic::new(6).unwrap();
let solo = hx.compute(&[&frame], &vec![nl.clone()]).unwrap();
let par = hx
.compute(&[&frame, &frame], &vec![nl.clone(), nl])
.unwrap();
assert_eq!(par.len(), 2);
assert_eq!(par[0].psi, solo[0].psi);
assert_eq!(par[1].psi, solo[0].psi);
}
}