molrs_compute/diffraction/
debye.rs1use molrs::store::frame_access::FrameAccess;
30use molrs::types::F;
31use ndarray::Array1;
32
33use crate::error::ComputeError;
34use crate::result::ComputeResult;
35use crate::traits::Compute;
36use crate::util::get_positions_ref;
37
38#[derive(Debug, Clone, Default)]
40pub struct StaticStructureFactorDebyeResult {
41 pub k_values: Array1<F>,
43 pub sk: Array1<F>,
45 pub n_particles: usize,
47}
48
49impl ComputeResult for StaticStructureFactorDebyeResult {}
50
51#[derive(Debug, Clone)]
53pub struct StaticStructureFactorDebye {
54 k_values: Array1<F>,
55}
56
57impl StaticStructureFactorDebye {
58 pub fn new(k_values: &[F]) -> Result<Self, ComputeError> {
61 if k_values.is_empty() {
62 return Err(ComputeError::OutOfRange {
63 field: "StaticStructureFactorDebye::k_values",
64 value: "empty".into(),
65 });
66 }
67 Ok(Self {
68 k_values: Array1::from_vec(k_values.to_vec()),
69 })
70 }
71
72 pub fn linspace(k_min: F, k_max: F, n: usize) -> Result<Self, ComputeError> {
75 if n == 0 {
76 return Err(ComputeError::OutOfRange {
77 field: "StaticStructureFactorDebye::linspace::n",
78 value: "0".into(),
79 });
80 }
81 if k_max < k_min {
82 return Err(ComputeError::OutOfRange {
83 field: "StaticStructureFactorDebye::linspace::k_max",
84 value: format!("k_max={k_max} < k_min={k_min}"),
85 });
86 }
87 let step = if n == 1 {
88 0.0
89 } else {
90 (k_max - k_min) / (n - 1) as F
91 };
92 let v: Vec<F> = (0..n).map(|i| k_min + i as F * step).collect();
93 Self::new(&v)
94 }
95
96 pub fn k_values(&self) -> &Array1<F> {
97 &self.k_values
98 }
99
100 fn one_frame<FA: FrameAccess>(
101 &self,
102 frame: &FA,
103 ) -> Result<StaticStructureFactorDebyeResult, ComputeError> {
104 let (xs_p, ys_p, zs_p) = get_positions_ref(frame)?;
105 let xs = xs_p.slice();
106 let ys = ys_p.slice();
107 let zs = zs_p.slice();
108 let n = xs.len();
109 let n_k = self.k_values.len();
110 let mut sk = Array1::<F>::zeros(n_k);
111
112 if n == 0 {
113 return Ok(StaticStructureFactorDebyeResult {
114 k_values: self.k_values.clone(),
115 sk,
116 n_particles: 0,
117 });
118 }
119
120 for v in sk.iter_mut() {
122 *v = n as F;
123 }
124
125 let inv_n = 1.0 / n as F;
128 for i in 0..n {
129 for j in (i + 1)..n {
130 let dx = xs[i] - xs[j];
131 let dy = ys[i] - ys[j];
132 let dz = zs[i] - zs[j];
133 let r = (dx * dx + dy * dy + dz * dz).sqrt();
134 if r == 0.0 {
135 for v in sk.iter_mut() {
137 *v += 2.0;
138 }
139 continue;
140 }
141 for (idx, &k) in self.k_values.iter().enumerate() {
142 let kr = k * r;
143 let term = if kr.abs() < 1e-9 { 1.0 } else { kr.sin() / kr };
144 sk[idx] += 2.0 * term;
145 }
146 }
147 }
148
149 for v in sk.iter_mut() {
151 *v *= inv_n;
152 }
153
154 Ok(StaticStructureFactorDebyeResult {
155 k_values: self.k_values.clone(),
156 sk,
157 n_particles: n,
158 })
159 }
160}
161
162impl Compute for StaticStructureFactorDebye {
163 type Args<'a> = ();
164 type Output = Vec<StaticStructureFactorDebyeResult>;
165
166 fn compute<'a, FA: FrameAccess + Sync + 'a>(
167 &self,
168 frames: &[&'a FA],
169 _: (),
170 ) -> Result<Vec<StaticStructureFactorDebyeResult>, ComputeError> {
171 if frames.is_empty() {
172 return Err(ComputeError::EmptyInput);
173 }
174 let mut out = Vec::with_capacity(frames.len());
175 for f in frames {
176 out.push(self.one_frame(*f)?);
177 }
178 Ok(out)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use molrs::Frame;
186 use molrs::spatial::region::simbox::SimBox;
187 use molrs::store::block::Block;
188 use ndarray::{Array1 as A1, array};
189
190 fn frame_with(positions: &[[F; 3]]) -> Frame {
191 let x = A1::from_iter(positions.iter().map(|p| p[0]));
192 let y = A1::from_iter(positions.iter().map(|p| p[1]));
193 let z = A1::from_iter(positions.iter().map(|p| p[2]));
194 let mut block = Block::new();
195 block.insert("x", x.into_dyn()).unwrap();
196 block.insert("y", y.into_dyn()).unwrap();
197 block.insert("z", z.into_dyn()).unwrap();
198 let mut frame = Frame::new();
199 frame.insert("atoms", block);
200 frame.simbox =
203 Some(SimBox::cube(1000.0, array![0.0 as F, 0.0 as F, 0.0 as F], [false; 3]).unwrap());
204 frame
205 }
206
207 const TOL: F = 1e-10;
208
209 #[test]
210 fn s_at_k_zero_equals_n() {
211 let positions = [[0.0_f64, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
212 let frame = frame_with(&positions);
213 let s = StaticStructureFactorDebye::new(&[0.0]).unwrap();
214 let r = &s.compute(&[&frame], ()).unwrap()[0];
215 assert!((r.sk[0] - 3.0).abs() < TOL);
217 assert_eq!(r.n_particles, 3);
218 }
219
220 #[test]
221 fn s_large_k_approaches_one() {
222 use rand::RngExt;
225 use rand::SeedableRng;
226 use rand::rngs::StdRng;
227 let mut rng = StdRng::seed_from_u64(7);
228 let n = 30;
229 let positions: Vec<[F; 3]> = (0..n)
230 .map(|_| {
231 [
232 rng.random::<F>() * 10.0,
233 rng.random::<F>() * 10.0,
234 rng.random::<F>() * 10.0,
235 ]
236 })
237 .collect();
238 let frame = frame_with(&positions);
239 let s = StaticStructureFactorDebye::linspace(50.0, 60.0, 11).unwrap();
240 let r = &s.compute(&[&frame], ()).unwrap()[0];
241 let mean: F = r.sk.iter().copied().sum::<F>() / r.sk.len() as F;
242 assert!(
243 (mean - 1.0).abs() < 0.2,
244 "S(k → ∞) should average to ≈ 1; got {mean}"
245 );
246 }
247
248 #[test]
249 fn two_particle_analytic() {
250 let d: F = 1.5;
254 let frame = frame_with(&[[0.0, 0.0, 0.0], [d, 0.0, 0.0]]);
255 let k_vals = [0.5_f64, 1.0, 2.0, 5.0];
256 let s = StaticStructureFactorDebye::new(&k_vals).unwrap();
257 let r = &s.compute(&[&frame], ()).unwrap()[0];
258 for (i, &k) in k_vals.iter().enumerate() {
259 let expected = 1.0 + (k * d).sin() / (k * d);
260 assert!(
261 (r.sk[i] - expected).abs() < 1e-12,
262 "k={k}: got {}, expected {expected}",
263 r.sk[i]
264 );
265 }
266 }
267
268 #[test]
269 fn coincident_particles_diagonal_only() {
270 let frame = frame_with(&[[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]]);
273 let s = StaticStructureFactorDebye::new(&[1.0, 5.0, 10.0]).unwrap();
274 let r = &s.compute(&[&frame], ()).unwrap()[0];
275 for &v in r.sk.iter() {
276 assert!((v - 2.0).abs() < TOL);
277 }
278 }
279
280 #[test]
281 fn empty_frame_gives_zero_sk() {
282 let frame = frame_with(&[]);
283 let s = StaticStructureFactorDebye::new(&[1.0]).unwrap();
284 let r = &s.compute(&[&frame], ()).unwrap()[0];
285 assert_eq!(r.sk[0], 0.0);
286 assert_eq!(r.n_particles, 0);
287 }
288
289 #[test]
290 fn invalid_k_array_errors() {
291 assert!(StaticStructureFactorDebye::new(&[]).is_err());
292 assert!(StaticStructureFactorDebye::linspace(2.0, 1.0, 10).is_err());
293 assert!(StaticStructureFactorDebye::linspace(0.0, 1.0, 0).is_err());
294 }
295
296 #[test]
297 fn linspace_n_one_returns_single_k() {
298 let s = StaticStructureFactorDebye::linspace(2.5, 7.0, 1).unwrap();
299 assert_eq!(s.k_values().len(), 1);
300 assert_eq!(s.k_values()[0], 2.5);
301 }
302
303 #[test]
304 fn multi_frame_returns_one_result_per_frame() {
305 let f1 = frame_with(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
306 let f2 = frame_with(&[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
307 let s = StaticStructureFactorDebye::new(&[1.0]).unwrap();
308 let r = s.compute(&[&f1, &f2], ()).unwrap();
309 assert_eq!(r.len(), 2);
310 assert!((r[0].sk[0] - r[1].sk[0]).abs() > 1e-3);
312 }
313}