use crate::compute::result::ComputeResult;
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use ndarray::Array1;
use crate::compute::error::ComputeError;
use crate::compute::traits::Compute;
use crate::compute::util::get_positions_ref;
#[derive(Debug, Clone)]
pub struct StaticStructureFactorDebye {
k_values: Array1<F>,
}
impl StaticStructureFactorDebye {
pub fn new(k_values: &[F]) -> Result<Self, ComputeError> {
if k_values.is_empty() {
return Err(ComputeError::OutOfRange {
field: "StaticStructureFactorDebye::k_values",
value: "empty".into(),
});
}
Ok(Self {
k_values: Array1::from_vec(k_values.to_vec()),
})
}
pub fn linspace(k_min: F, k_max: F, n: usize) -> Result<Self, ComputeError> {
if n == 0 {
return Err(ComputeError::OutOfRange {
field: "StaticStructureFactorDebye::linspace::n",
value: "0".into(),
});
}
if k_max < k_min {
return Err(ComputeError::OutOfRange {
field: "StaticStructureFactorDebye::linspace::k_max",
value: format!("k_max={k_max} < k_min={k_min}"),
});
}
let step = if n == 1 {
0.0
} else {
(k_max - k_min) / (n - 1) as F
};
let v: Vec<F> = (0..n).map(|i| k_min + i as F * step).collect();
Self::new(&v)
}
pub fn k_values(&self) -> &Array1<F> {
&self.k_values
}
fn one_frame<FA: FrameAccess>(
&self,
frame: &FA,
) -> Result<StaticStructureFactorDebyeResult, ComputeError> {
let (xs_p, ys_p, zs_p) = get_positions_ref(frame)?;
let xs = xs_p.slice();
let ys = ys_p.slice();
let zs = zs_p.slice();
let n = xs.len();
let n_k = self.k_values.len();
let mut sk = Array1::<F>::zeros(n_k);
if n == 0 {
return Ok(StaticStructureFactorDebyeResult {
k_values: self.k_values.clone(),
sk,
n_particles: 0,
});
}
for v in sk.iter_mut() {
*v = n as F;
}
let inv_n = 1.0 / n as F;
#[cfg(feature = "rayon")]
{
use rayon::prelude::*;
let k_values = &self.k_values;
let off = (0..n)
.into_par_iter()
.fold(
|| Array1::<F>::zeros(n_k),
|mut acc, i| {
accumulate_row(&mut acc, xs, ys, zs, k_values, i);
acc
},
)
.reduce(
|| Array1::<F>::zeros(n_k),
|mut a, b| {
a += &b;
a
},
);
sk += &off;
}
#[cfg(not(feature = "rayon"))]
for i in 0..n {
accumulate_row(&mut sk, xs, ys, zs, &self.k_values, i);
}
for v in sk.iter_mut() {
*v *= inv_n;
}
Ok(StaticStructureFactorDebyeResult {
k_values: self.k_values.clone(),
sk,
n_particles: n,
})
}
}
fn accumulate_row(
acc: &mut Array1<F>,
xs: &[F],
ys: &[F],
zs: &[F],
k_values: &Array1<F>,
i: usize,
) {
let n = xs.len();
for j in (i + 1)..n {
let dx = xs[i] - xs[j];
let dy = ys[i] - ys[j];
let dz = zs[i] - zs[j];
let r = (dx * dx + dy * dy + dz * dz).sqrt();
if r == 0.0 {
for v in acc.iter_mut() {
*v += 2.0;
}
continue;
}
for (idx, &k) in k_values.iter().enumerate() {
let kr = k * r;
let term = if kr.abs() < 1e-9 { 1.0 } else { kr.sin() / kr };
acc[idx] += 2.0 * term;
}
}
}
impl Compute for StaticStructureFactorDebye {
type Args<'a> = ();
type Output = Vec<StaticStructureFactorDebyeResult>;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
_: (),
) -> Result<Vec<StaticStructureFactorDebyeResult>, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
let mut out = Vec::with_capacity(frames.len());
for f in frames {
out.push(self.one_frame(*f)?);
}
Ok(out)
}
}
#[derive(Debug, Clone, Default)]
pub struct StaticStructureFactorDebyeResult {
pub k_values: Array1<F>,
pub sk: Array1<F>,
pub n_particles: usize,
}
impl ComputeResult for StaticStructureFactorDebyeResult {}
#[cfg(test)]
mod tests {
use super::*;
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]]) -> 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(1000.0, array![0.0 as F, 0.0 as F, 0.0 as F], [false; 3]).unwrap());
frame
}
const TOL: F = 1e-10;
#[test]
fn s_at_k_zero_equals_n() {
let positions = [[0.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let frame = frame_with(&positions);
let s = StaticStructureFactorDebye::new(&[0.0]).unwrap();
let r = &s.compute(&[&frame], ()).unwrap()[0];
assert!((r.sk[0] - 3.0).abs() < TOL);
assert_eq!(r.n_particles, 3);
}
#[test]
fn s_large_k_approaches_one() {
use rand::RngExt;
use rand::SeedableRng;
use rand::rngs::StdRng;
let mut rng = StdRng::seed_from_u64(7);
let n = 30;
let positions: Vec<[F; 3]> = (0..n)
.map(|_| {
[
rng.random::<F>() * 10.0,
rng.random::<F>() * 10.0,
rng.random::<F>() * 10.0,
]
})
.collect();
let frame = frame_with(&positions);
let s = StaticStructureFactorDebye::linspace(50.0, 60.0, 11).unwrap();
let r = &s.compute(&[&frame], ()).unwrap()[0];
let mean: F = r.sk.iter().copied().sum::<F>() / r.sk.len() as F;
assert!(
(mean - 1.0).abs() < 0.2,
"S(k → ∞) should average to ≈ 1; got {mean}"
);
}
#[test]
fn two_particle_analytic() {
let d: F = 1.5;
let frame = frame_with(&[[0.0, 0.0, 0.0], [d, 0.0, 0.0]]);
let k_vals = [0.5_f64, 1.0, 2.0, 5.0];
let s = StaticStructureFactorDebye::new(&k_vals).unwrap();
let r = &s.compute(&[&frame], ()).unwrap()[0];
for (i, &k) in k_vals.iter().enumerate() {
let expected = 1.0 + (k * d).sin() / (k * d);
assert!(
(r.sk[i] - expected).abs() < 1e-12,
"k={k}: got {}, expected {expected}",
r.sk[i]
);
}
}
#[test]
fn coincident_particles_diagonal_only() {
let frame = frame_with(&[[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]]);
let s = StaticStructureFactorDebye::new(&[1.0, 5.0, 10.0]).unwrap();
let r = &s.compute(&[&frame], ()).unwrap()[0];
for &v in r.sk.iter() {
assert!((v - 2.0).abs() < TOL);
}
}
#[test]
fn empty_frame_gives_zero_sk() {
let frame = frame_with(&[]);
let s = StaticStructureFactorDebye::new(&[1.0]).unwrap();
let r = &s.compute(&[&frame], ()).unwrap()[0];
assert_eq!(r.sk[0], 0.0);
assert_eq!(r.n_particles, 0);
}
#[test]
fn invalid_k_array_errors() {
assert!(StaticStructureFactorDebye::new(&[]).is_err());
assert!(StaticStructureFactorDebye::linspace(2.0, 1.0, 10).is_err());
assert!(StaticStructureFactorDebye::linspace(0.0, 1.0, 0).is_err());
}
#[test]
fn linspace_n_one_returns_single_k() {
let s = StaticStructureFactorDebye::linspace(2.5, 7.0, 1).unwrap();
assert_eq!(s.k_values().len(), 1);
assert_eq!(s.k_values()[0], 2.5);
}
#[test]
fn multi_frame_returns_one_result_per_frame() {
let f1 = frame_with(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
let f2 = frame_with(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
let s = StaticStructureFactorDebye::new(&[1.0]).unwrap();
let r = s.compute(&[&f1, &f2], ()).unwrap();
assert_eq!(r.len(), 2);
assert!((r[0].sk[0] - r[1].sk[0]).abs() > 1e-3);
}
}