molrs_compute/diffraction/
direct.rs1use molrs::spatial::region::simbox::BoxKind;
25use molrs::store::frame_access::FrameAccess;
26use molrs::types::F;
27use ndarray::Array1;
28
29use crate::error::ComputeError;
30use crate::result::ComputeResult;
31use crate::traits::Compute;
32use crate::util::get_positions_ref;
33
34const TWO_PI: F = 2.0 * std::f64::consts::PI;
35
36#[derive(Debug, Clone, Default)]
45pub struct StaticStructureFactorDirectResult {
46 pub k_magnitudes: Array1<F>,
47 pub sk: Array1<F>,
48 pub n_particles: usize,
49}
50
51impl ComputeResult for StaticStructureFactorDirectResult {}
52
53#[derive(Debug, Clone)]
54enum KMode {
55 Explicit { k_vecs: Vec<[F; 3]> },
57 Isotropic { k_max: F, n_bins: usize },
59}
60
61#[derive(Debug, Clone)]
62pub struct StaticStructureFactorDirect {
63 mode: KMode,
64}
65
66impl StaticStructureFactorDirect {
67 pub fn new(k_vecs: &[[F; 3]]) -> Result<Self, ComputeError> {
69 if k_vecs.is_empty() {
70 return Err(ComputeError::OutOfRange {
71 field: "StaticStructureFactorDirect::k_vecs",
72 value: "empty".into(),
73 });
74 }
75 Ok(Self {
76 mode: KMode::Explicit {
77 k_vecs: k_vecs.to_vec(),
78 },
79 })
80 }
81
82 pub fn isotropic(k_max: F, n_bins: usize) -> Result<Self, ComputeError> {
86 if k_max.is_nan() || k_max <= 0.0 || n_bins == 0 {
87 return Err(ComputeError::OutOfRange {
88 field: "StaticStructureFactorDirect::isotropic",
89 value: format!("k_max={k_max}, n_bins={n_bins}"),
90 });
91 }
92 Ok(Self {
93 mode: KMode::Isotropic { k_max, n_bins },
94 })
95 }
96
97 fn evaluate_explicit<FA: FrameAccess>(
98 frame: &FA,
99 k_vecs: &[[F; 3]],
100 ) -> Result<StaticStructureFactorDirectResult, ComputeError> {
101 let (xs_p, ys_p, zs_p) = get_positions_ref(frame)?;
102 let xs = xs_p.slice();
103 let ys = ys_p.slice();
104 let zs = zs_p.slice();
105 let n = xs.len();
106 let inv_n = if n > 0 { 1.0 / n as F } else { 0.0 };
107
108 let mut sk = Array1::<F>::zeros(k_vecs.len());
109 let mut kmags = Array1::<F>::zeros(k_vecs.len());
110 for (idx, k) in k_vecs.iter().enumerate() {
111 kmags[idx] = (k[0] * k[0] + k[1] * k[1] + k[2] * k[2]).sqrt();
112 let mut re: F = 0.0;
113 let mut im: F = 0.0;
114 for j in 0..n {
115 let phase = k[0] * xs[j] + k[1] * ys[j] + k[2] * zs[j];
116 re += phase.cos();
117 im += phase.sin();
118 }
119 sk[idx] = inv_n * (re * re + im * im);
120 }
121 Ok(StaticStructureFactorDirectResult {
122 k_magnitudes: kmags,
123 sk,
124 n_particles: n,
125 })
126 }
127
128 fn evaluate_isotropic<FA: FrameAccess>(
129 frame: &FA,
130 k_max: F,
131 n_bins: usize,
132 ) -> Result<StaticStructureFactorDirectResult, ComputeError> {
133 let simbox = frame.simbox_ref().ok_or(ComputeError::MissingSimBox)?;
134 let (lx, ly, lz) = match simbox.kind() {
135 BoxKind::Ortho { len, .. } => (len[0], len[1], len[2]),
136 BoxKind::Triclinic => {
137 return Err(ComputeError::OutOfRange {
138 field: "StaticStructureFactorDirect::isotropic::simbox",
139 value: "triclinic boxes not supported".into(),
140 });
141 }
142 };
143 let dkx = TWO_PI / lx;
144 let dky = TWO_PI / ly;
145 let dkz = TWO_PI / lz;
146 let nx = (k_max / dkx).ceil() as i32;
147 let ny = (k_max / dky).ceil() as i32;
148 let nz = (k_max / dkz).ceil() as i32;
149
150 let (xs_p, ys_p, zs_p) = get_positions_ref(frame)?;
151 let xs = xs_p.slice();
152 let ys = ys_p.slice();
153 let zs = zs_p.slice();
154 let n_atoms = xs.len();
155 let inv_n = if n_atoms > 0 { 1.0 / n_atoms as F } else { 0.0 };
156
157 let dk = k_max / n_bins as F;
158 let mut sk_sum = vec![0.0_f64; n_bins];
159 let mut counts = vec![0_u64; n_bins];
160
161 for ix in -nx..=nx {
162 for iy in -ny..=ny {
163 for iz in -nz..=nz {
164 if ix == 0 && iy == 0 && iz == 0 {
165 continue;
166 }
167 let kx = ix as F * dkx;
168 let ky = iy as F * dky;
169 let kz = iz as F * dkz;
170 let kmag = (kx * kx + ky * ky + kz * kz).sqrt();
171 if kmag > k_max || kmag <= 0.0 {
172 continue;
173 }
174 let bin = ((kmag / dk) as usize).min(n_bins - 1);
175 let mut re: F = 0.0;
176 let mut im: F = 0.0;
177 for j in 0..n_atoms {
178 let phase = kx * xs[j] + ky * ys[j] + kz * zs[j];
179 re += phase.cos();
180 im += phase.sin();
181 }
182 sk_sum[bin] += inv_n * (re * re + im * im);
183 counts[bin] += 1;
184 }
185 }
186 }
187
188 let mut sk = Array1::<F>::zeros(n_bins);
189 let mut kmags = Array1::<F>::zeros(n_bins);
190 for b in 0..n_bins {
191 kmags[b] = (b as F + 0.5) * dk;
192 if counts[b] > 0 {
193 sk[b] = sk_sum[b] / counts[b] as F;
194 }
195 }
196 Ok(StaticStructureFactorDirectResult {
197 k_magnitudes: kmags,
198 sk,
199 n_particles: n_atoms,
200 })
201 }
202}
203
204impl Compute for StaticStructureFactorDirect {
205 type Args<'a> = ();
206 type Output = Vec<StaticStructureFactorDirectResult>;
207
208 fn compute<'a, FA: FrameAccess + Sync + 'a>(
209 &self,
210 frames: &[&'a FA],
211 _: (),
212 ) -> Result<Vec<StaticStructureFactorDirectResult>, ComputeError> {
213 if frames.is_empty() {
214 return Err(ComputeError::EmptyInput);
215 }
216 let mut out = Vec::with_capacity(frames.len());
217 for f in frames {
218 let r = match &self.mode {
219 KMode::Explicit { k_vecs } => Self::evaluate_explicit(*f, k_vecs)?,
220 KMode::Isotropic { k_max, n_bins } => {
221 Self::evaluate_isotropic(*f, *k_max, *n_bins)?
222 }
223 };
224 out.push(r);
225 }
226 Ok(out)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use molrs::Frame;
234 use molrs::spatial::region::simbox::SimBox;
235 use molrs::store::block::Block;
236 use ndarray::{Array1 as A1, array};
237
238 fn frame_with(positions: &[[F; 3]], box_len: F, pbc: [bool; 3]) -> Frame {
239 let x = A1::from_iter(positions.iter().map(|p| p[0]));
240 let y = A1::from_iter(positions.iter().map(|p| p[1]));
241 let z = A1::from_iter(positions.iter().map(|p| p[2]));
242 let mut block = Block::new();
243 block.insert("x", x.into_dyn()).unwrap();
244 block.insert("y", y.into_dyn()).unwrap();
245 block.insert("z", z.into_dyn()).unwrap();
246 let mut frame = Frame::new();
247 frame.insert("atoms", block);
248 frame.simbox =
249 Some(SimBox::cube(box_len, array![0.0 as F, 0.0 as F, 0.0 as F], pbc).unwrap());
250 frame
251 }
252
253 const TOL: F = 1e-10;
254
255 #[test]
256 fn s_at_zero_k_equals_n() {
257 let frame = frame_with(
259 &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]],
260 10.0,
261 [false; 3],
262 );
263 let r = &StaticStructureFactorDirect::new(&[[0.0, 0.0, 0.0]])
264 .unwrap()
265 .compute(&[&frame], ())
266 .unwrap()[0];
267 assert!((r.sk[0] - 3.0).abs() < TOL);
268 }
269
270 #[test]
271 fn two_particle_analytic_explicit() {
272 let d = 1.5_f64;
277 let frame = frame_with(&[[0.0, 0.0, 0.0], [d, 0.0, 0.0]], 10.0, [false; 3]);
278 let kx_vals = [0.5_f64, 1.2, 2.7];
279 let k_vecs: Vec<[F; 3]> = kx_vals.iter().map(|&k| [k, 0.0, 0.0]).collect();
280 let r = &StaticStructureFactorDirect::new(&k_vecs)
281 .unwrap()
282 .compute(&[&frame], ())
283 .unwrap()[0];
284 for (i, &k) in kx_vals.iter().enumerate() {
285 let expected = 1.0 + (k * d).cos();
286 assert!(
287 (r.sk[i] - expected).abs() < TOL,
288 "k={k}: got {}, expected {expected}",
289 r.sk[i]
290 );
291 }
292 }
293
294 #[test]
295 fn isotropic_has_bragg_peak_for_lattice() {
296 let mut positions = Vec::new();
300 for ix in 0..4 {
301 for iy in 0..4 {
302 for iz in 0..4 {
303 positions.push([ix as F, iy as F, iz as F]);
304 }
305 }
306 }
307 let frame = frame_with(&positions, 4.0, [true, true, true]);
308 let r = &StaticStructureFactorDirect::isotropic(8.0, 16)
309 .unwrap()
310 .compute(&[&frame], ())
311 .unwrap()[0];
312 let mut max_sk = 0.0_f64;
314 let mut max_k = 0.0_f64;
315 for b in 0..16 {
316 let k = r.k_magnitudes[b];
317 if (5.5..=7.0).contains(&k) && r.sk[b] > max_sk {
318 max_sk = r.sk[b];
319 max_k = k;
320 }
321 }
322 assert!(
324 max_sk > 5.0,
325 "expected a Bragg peak near k = 2π with S ≫ 1, got S({max_k}) = {max_sk}",
326 );
327 }
328
329 #[test]
330 fn invalid_inputs_error() {
331 assert!(StaticStructureFactorDirect::new(&[]).is_err());
332 assert!(StaticStructureFactorDirect::isotropic(0.0, 10).is_err());
333 assert!(StaticStructureFactorDirect::isotropic(1.0, 0).is_err());
334 }
335
336 #[test]
337 fn empty_frame_returns_zero_sk() {
338 let frame = frame_with(&[], 10.0, [false; 3]);
339 let r = &StaticStructureFactorDirect::new(&[[1.0, 0.0, 0.0]])
340 .unwrap()
341 .compute(&[&frame], ())
342 .unwrap()[0];
343 assert_eq!(r.n_particles, 0);
344 assert_eq!(r.sk[0], 0.0);
345 }
346}