1#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions::*;
8use crate::xdmf;
9use oxiphysics_core::math::Vec3;
10use std::io::Write;
11
12#[allow(dead_code)]
16pub struct VacfCalculator;
17#[allow(dead_code)]
18impl VacfCalculator {
19 pub fn compute_normalized(frames: &[VelocityFrame], max_lag: usize) -> Vec<(usize, f64)> {
23 if frames.is_empty() {
24 return Vec::new();
25 }
26 let effective_lag = max_lag.min(frames.len() - 1);
27 let n_atoms = frames[0].n_atoms();
28 let c0 = Self::vacf_at_lag(frames, 0, n_atoms);
29 if c0.abs() < 1e-30 {
30 return (0..=effective_lag).map(|l| (l, 0.0)).collect();
31 }
32 (0..=effective_lag)
33 .map(|lag| {
34 let c = Self::vacf_at_lag(frames, lag, n_atoms);
35 (lag, c / c0)
36 })
37 .collect()
38 }
39 fn vacf_at_lag(frames: &[VelocityFrame], lag: usize, n_atoms: usize) -> f64 {
40 let mut sum = 0.0_f64;
41 let mut count = 0_usize;
42 for t0 in 0..(frames.len().saturating_sub(lag)) {
43 let t1 = t0 + lag;
44 let n = n_atoms.min(frames[t0].n_atoms()).min(frames[t1].n_atoms());
45 for i in 0..n {
46 let v0 = frames[t0].velocities[i];
47 let v1 = frames[t1].velocities[i];
48 sum += v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
49 count += 1;
50 }
51 }
52 if count > 0 { sum / count as f64 } else { 0.0 }
53 }
54 pub fn integrate_vacf(vacf: &[(usize, f64)], dt: f64) -> f64 {
59 if vacf.len() < 2 || dt <= 0.0 {
60 return 0.0;
61 }
62 let mut integral = 0.0_f64;
63 for i in 0..vacf.len() - 1 {
64 integral += (vacf[i].1 + vacf[i + 1].1) * 0.5 * dt;
65 }
66 integral / 3.0
67 }
68}
69#[allow(dead_code)]
71pub struct XyzReader;
72#[allow(dead_code)]
73impl XyzReader {
74 pub fn read_frame(data: &str) -> Result<TrajectoryFrame, crate::Error> {
76 let mut lines = data.lines();
77 let count_line = lines
78 .next()
79 .ok_or_else(|| crate::Error::Parse("XYZ: empty input".to_string()))?;
80 let n_atoms: usize = count_line
81 .trim()
82 .parse()
83 .map_err(|_| crate::Error::Parse(format!("XYZ: bad atom count '{}'", count_line)))?;
84 let comment = lines
85 .next()
86 .ok_or_else(|| crate::Error::Parse("XYZ: missing comment line".to_string()))?;
87 let (timestep, time) = parse_xyz_comment(comment);
88 let mut positions = Vec::with_capacity(n_atoms);
89 let mut atom_types = Vec::with_capacity(n_atoms);
90 for i in 0..n_atoms {
91 let line = lines.next().ok_or_else(|| {
92 crate::Error::Parse(format!("XYZ: missing atom line {} of {}", i + 1, n_atoms))
93 })?;
94 let parts: Vec<&str> = line.split_whitespace().collect();
95 if parts.len() < 4 {
96 return Err(crate::Error::Parse(format!(
97 "XYZ: atom line {} has only {} fields",
98 i,
99 parts.len()
100 )));
101 }
102 atom_types.push(parts[0].to_string());
103 let x: f64 = parts[1]
104 .parse()
105 .map_err(|_| crate::Error::Parse(format!("XYZ: bad x coord on atom line {}", i)))?;
106 let y: f64 = parts[2]
107 .parse()
108 .map_err(|_| crate::Error::Parse(format!("XYZ: bad y coord on atom line {}", i)))?;
109 let z: f64 = parts[3]
110 .parse()
111 .map_err(|_| crate::Error::Parse(format!("XYZ: bad z coord on atom line {}", i)))?;
112 positions.push([x, y, z]);
113 }
114 Ok(TrajectoryFrame {
115 timestep,
116 time,
117 positions,
118 atom_types,
119 })
120 }
121 pub fn read_all_frames(data: &str) -> Result<Vec<TrajectoryFrame>, crate::Error> {
123 let mut frames = Vec::new();
124 let mut remaining = data;
125 loop {
126 remaining = remaining.trim_start_matches('\n');
127 remaining = remaining.trim_start_matches('\r');
128 if remaining.is_empty() {
129 break;
130 }
131 let mut it = remaining.lines();
132 let count_line = match it.next() {
133 Some(l) => l,
134 None => break,
135 };
136 let n_atoms: usize = count_line.trim().parse().map_err(|_| {
137 crate::Error::Parse(format!("XYZ: bad atom count '{}'", count_line))
138 })?;
139 let frame_lines = 2 + n_atoms;
140 let frame_text: String = remaining
141 .lines()
142 .take(frame_lines)
143 .collect::<Vec<&str>>()
144 .join("\n");
145 let frame = Self::read_frame(&frame_text)?;
146 frames.push(frame);
147 let consumed = count_line_bytes(remaining, frame_lines);
148 remaining = &remaining[consumed..];
149 }
150 Ok(frames)
151 }
152}
153#[allow(dead_code)]
155pub struct TrajectoryStatistics;
156#[allow(dead_code)]
157impl TrajectoryStatistics {
158 pub fn mean_positions(frames: &[TrajectoryFrame]) -> Vec<[f64; 3]> {
160 if frames.is_empty() {
161 return Vec::new();
162 }
163 let n_atoms = frames[0].n_atoms();
164 let mut mean = vec![[0.0_f64; 3]; n_atoms];
165 let mut count = vec![0_usize; n_atoms];
166 for frame in frames {
167 let n = n_atoms.min(frame.n_atoms());
168 for i in 0..n {
169 mean[i][0] += frame.positions[i][0];
170 mean[i][1] += frame.positions[i][1];
171 mean[i][2] += frame.positions[i][2];
172 count[i] += 1;
173 }
174 }
175 for i in 0..n_atoms {
176 if count[i] > 0 {
177 let c = count[i] as f64;
178 mean[i][0] /= c;
179 mean[i][1] /= c;
180 mean[i][2] /= c;
181 }
182 }
183 mean
184 }
185 pub fn rmsf(frames: &[TrajectoryFrame]) -> Vec<f64> {
187 if frames.is_empty() {
188 return Vec::new();
189 }
190 let mean = Self::mean_positions(frames);
191 let n_atoms = mean.len();
192 let mut sum_sq = vec![0.0_f64; n_atoms];
193 let mut count = vec![0_usize; n_atoms];
194 for frame in frames {
195 let n = n_atoms.min(frame.n_atoms());
196 for i in 0..n {
197 let dx = frame.positions[i][0] - mean[i][0];
198 let dy = frame.positions[i][1] - mean[i][1];
199 let dz = frame.positions[i][2] - mean[i][2];
200 sum_sq[i] += dx * dx + dy * dy + dz * dz;
201 count[i] += 1;
202 }
203 }
204 sum_sq
205 .iter()
206 .zip(count.iter())
207 .map(|(&sq, &c)| if c > 0 { (sq / c as f64).sqrt() } else { 0.0 })
208 .collect()
209 }
210 pub fn radius_of_gyration(frame: &TrajectoryFrame, masses: &[f64]) -> f64 {
212 let n = frame.n_atoms().min(masses.len());
213 if n == 0 {
214 return 0.0;
215 }
216 let com = center_of_mass(&frame.positions[..n], &masses[..n]);
217 let total_mass: f64 = masses[..n].iter().sum();
218 if total_mass < 1e-30 {
219 return 0.0;
220 }
221 let mut sum = 0.0_f64;
222 for i in 0..n {
223 let dx = frame.positions[i][0] - com[0];
224 let dy = frame.positions[i][1] - com[1];
225 let dz = frame.positions[i][2] - com[2];
226 sum += masses[i] * (dx * dx + dy * dy + dz * dz);
227 }
228 (sum / total_mass).sqrt()
229 }
230 pub fn end_to_end_distance(frame: &TrajectoryFrame) -> f64 {
232 if frame.n_atoms() < 2 {
233 return 0.0;
234 }
235 let first = frame.positions[0];
236 let last = frame.positions[frame.n_atoms() - 1];
237 let dx = last[0] - first[0];
238 let dy = last[1] - first[1];
239 let dz = last[2] - first[2];
240 (dx * dx + dy * dy + dz * dz).sqrt()
241 }
242}
243#[allow(dead_code)]
245pub struct RdfCalculator;
246#[allow(dead_code)]
247impl RdfCalculator {
248 pub fn compute(
253 frames: &[TrajectoryFrame],
254 r_max: f64,
255 n_bins: usize,
256 box_size: [f64; 3],
257 ) -> (Vec<f64>, Vec<f64>) {
258 if frames.is_empty() || n_bins == 0 || r_max <= 0.0 {
259 return (Vec::new(), Vec::new());
260 }
261 let dr = r_max / n_bins as f64;
262 let mut hist = vec![0_u64; n_bins];
263 let mut n_frames = 0_u64;
264 let mut n_atoms_total = 0_u64;
265 for frame in frames {
266 let n = frame.n_atoms();
267 if n < 2 {
268 continue;
269 }
270 n_frames += 1;
271 n_atoms_total += n as u64;
272 for i in 0..n {
273 for j in (i + 1)..n {
274 let mut dx = frame.positions[j][0] - frame.positions[i][0];
275 let mut dy = frame.positions[j][1] - frame.positions[i][1];
276 let mut dz = frame.positions[j][2] - frame.positions[i][2];
277 for (d, l) in [
278 (&mut dx, box_size[0]),
279 (&mut dy, box_size[1]),
280 (&mut dz, box_size[2]),
281 ] {
282 if l > 1e-30 {
283 *d -= (*d / l).round() * l;
284 }
285 }
286 let r = (dx * dx + dy * dy + dz * dz).sqrt();
287 if r < r_max {
288 let bin = (r / dr) as usize;
289 if bin < n_bins {
290 hist[bin] += 2;
291 }
292 }
293 }
294 }
295 }
296 let vol = box_size[0] * box_size[1] * box_size[2];
297 let avg_n = if n_frames > 0 {
298 n_atoms_total as f64 / n_frames as f64
299 } else {
300 1.0
301 };
302 let rho = avg_n / vol;
303 let r_vals: Vec<f64> = (0..n_bins).map(|b| (b as f64 + 0.5) * dr).collect();
304 let g_vals: Vec<f64> = (0..n_bins)
305 .map(|b| {
306 let r = r_vals[b];
307 let shell_vol = 4.0 * std::f64::consts::PI * r * r * dr;
308 let ideal = rho * shell_vol * avg_n;
309 if ideal < 1e-30 || n_frames == 0 {
310 0.0
311 } else {
312 hist[b] as f64 / (n_frames as f64 * ideal)
313 }
314 })
315 .collect();
316 (r_vals, g_vals)
317 }
318}
319#[allow(dead_code)]
325pub struct MsdCalculator;
326#[allow(dead_code)]
327impl MsdCalculator {
328 pub fn compute(frames: &[TrajectoryFrame], max_lag: usize) -> Vec<(usize, f64)> {
332 if frames.len() < 2 || max_lag == 0 {
333 return vec![(0, 0.0)];
334 }
335 let n_atoms = frames[0].n_atoms();
336 let effective_lag = max_lag.min(frames.len() - 1);
337 let mut result = Vec::with_capacity(effective_lag + 1);
338 for lag in 0..=effective_lag {
339 let mut sum = 0.0_f64;
340 let mut count = 0_usize;
341 for t0 in 0..(frames.len() - lag) {
342 let t1 = t0 + lag;
343 let n = n_atoms.min(frames[t0].n_atoms()).min(frames[t1].n_atoms());
344 for i in 0..n {
345 let dx = frames[t1].positions[i][0] - frames[t0].positions[i][0];
346 let dy = frames[t1].positions[i][1] - frames[t0].positions[i][1];
347 let dz = frames[t1].positions[i][2] - frames[t0].positions[i][2];
348 sum += dx * dx + dy * dy + dz * dz;
349 count += 1;
350 }
351 }
352 let msd = if count > 0 { sum / count as f64 } else { 0.0 };
353 result.push((lag, msd));
354 }
355 result
356 }
357 pub fn diffusion_coefficient(msd_data: &[(usize, f64)], dt: f64) -> f64 {
364 if msd_data.len() < 2 || dt <= 0.0 {
365 return 0.0;
366 }
367 let n = msd_data.len() as f64;
368 let sum_x: f64 = msd_data.iter().map(|(lag, _)| *lag as f64 * dt).sum();
369 let sum_y: f64 = msd_data.iter().map(|(_, msd)| *msd).sum();
370 let sum_xy: f64 = msd_data
371 .iter()
372 .map(|(lag, msd)| *lag as f64 * dt * msd)
373 .sum();
374 let sum_x2: f64 = msd_data
375 .iter()
376 .map(|(lag, _)| (*lag as f64 * dt).powi(2))
377 .sum();
378 let denom = n * sum_x2 - sum_x * sum_x;
379 if denom.abs() < 1e-30 {
380 return 0.0;
381 }
382 let slope = (n * sum_xy - sum_x * sum_y) / denom;
383 slope / 6.0
384 }
385}
386#[allow(dead_code)]
388pub struct BondLengthAnalyser;
389#[allow(dead_code)]
390impl BondLengthAnalyser {
391 pub fn bond_length(frame: &TrajectoryFrame, i: usize, j: usize) -> f64 {
393 let pi = frame.positions[i];
394 let pj = frame.positions[j];
395 let dx = pi[0] - pj[0];
396 let dy = pi[1] - pj[1];
397 let dz = pi[2] - pj[2];
398 (dx * dx + dy * dy + dz * dz).sqrt()
399 }
400 pub fn time_series(frames: &[TrajectoryFrame], i: usize, j: usize) -> Vec<f64> {
402 frames
403 .iter()
404 .filter(|f| f.n_atoms() > i.max(j))
405 .map(|f| Self::bond_length(f, i, j))
406 .collect()
407 }
408 pub fn mean_and_std(frames: &[TrajectoryFrame], i: usize, j: usize) -> (f64, f64) {
410 let series = Self::time_series(frames, i, j);
411 if series.is_empty() {
412 return (0.0, 0.0);
413 }
414 let mean = series.iter().sum::<f64>() / series.len() as f64;
415 let var =
416 series.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / series.len() as f64;
417 (mean, var.sqrt())
418 }
419 pub fn bond_angle_deg(frame: &TrajectoryFrame, i: usize, j: usize, k: usize) -> f64 {
423 let pi = frame.positions[i];
424 let pj = frame.positions[j];
425 let pk = frame.positions[k];
426 let v1 = [pi[0] - pj[0], pi[1] - pj[1], pi[2] - pj[2]];
427 let v2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
428 let l1 = (v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2]).sqrt();
429 let l2 = (v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2]).sqrt();
430 if l1 < 1e-30 || l2 < 1e-30 {
431 return 0.0;
432 }
433 let cos_theta = (v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]) / (l1 * l2);
434 cos_theta.clamp(-1.0, 1.0).acos().to_degrees()
435 }
436}
437#[allow(dead_code)]
439pub struct TrajectoryResampler;
440#[allow(dead_code)]
441impl TrajectoryResampler {
442 pub fn resample_uniform(frames: &[TrajectoryFrame], target_dt: f64) -> Vec<TrajectoryFrame> {
447 if frames.is_empty() || target_dt <= 0.0 {
448 return Vec::new();
449 }
450 if frames.len() == 1 {
451 return vec![frames[0].clone()];
452 }
453 let t_start = frames[0].time;
454 let t_end = frames.last().expect("collection should not be empty").time;
455 if t_end <= t_start {
456 return vec![frames[0].clone()];
457 }
458 let n_samples = ((t_end - t_start) / target_dt).ceil() as usize + 1;
459 let mut result = Vec::with_capacity(n_samples);
460 for s in 0..n_samples {
461 let t = t_start + s as f64 * target_dt;
462 if t > t_end {
463 break;
464 }
465 let frame = Self::interpolate_at_time(frames, t);
466 result.push(frame);
467 }
468 result
469 }
470 fn interpolate_at_time(frames: &[TrajectoryFrame], t: f64) -> TrajectoryFrame {
472 let mut lo = 0;
473 let mut hi = frames.len() - 1;
474 for i in 0..frames.len() - 1 {
475 if frames[i].time <= t && frames[i + 1].time >= t {
476 lo = i;
477 hi = i + 1;
478 break;
479 }
480 }
481 if lo == hi || (frames[hi].time - frames[lo].time).abs() < 1e-30 {
482 return frames[lo].clone();
483 }
484 let alpha = (t - frames[lo].time) / (frames[hi].time - frames[lo].time);
485 let alpha = alpha.clamp(0.0, 1.0);
486 let n_atoms = frames[lo].n_atoms().min(frames[hi].n_atoms());
487 let mut positions = Vec::with_capacity(n_atoms);
488 for i in 0..n_atoms {
489 positions.push([
490 frames[lo].positions[i][0] * (1.0 - alpha) + frames[hi].positions[i][0] * alpha,
491 frames[lo].positions[i][1] * (1.0 - alpha) + frames[hi].positions[i][1] * alpha,
492 frames[lo].positions[i][2] * (1.0 - alpha) + frames[hi].positions[i][2] * alpha,
493 ]);
494 }
495 TrajectoryFrame {
496 timestep: (t / 1.0) as u64,
497 time: t,
498 positions,
499 atom_types: frames[lo].atom_types[..n_atoms].to_vec(),
500 }
501 }
502 pub fn subsample(frames: &[TrajectoryFrame], every_n: usize) -> Vec<TrajectoryFrame> {
504 if every_n == 0 {
505 return Vec::new();
506 }
507 frames.iter().step_by(every_n).cloned().collect()
508 }
509}
510#[derive(Debug, Clone)]
512#[allow(dead_code)]
513pub struct TrajectoryFrame {
514 pub timestep: u64,
516 pub time: f64,
518 pub positions: Vec<[f64; 3]>,
520 pub atom_types: Vec<String>,
522}
523#[allow(dead_code)]
524impl TrajectoryFrame {
525 pub fn new(
527 timestep: u64,
528 time: f64,
529 positions: Vec<[f64; 3]>,
530 atom_types: Vec<String>,
531 ) -> Self {
532 Self {
533 timestep,
534 time,
535 positions,
536 atom_types,
537 }
538 }
539 pub fn n_atoms(&self) -> usize {
541 self.positions.len()
542 }
543}
544#[allow(dead_code)]
554pub struct XyzWriter;
555#[allow(dead_code)]
556impl XyzWriter {
557 pub fn write_frame(frame: &TrajectoryFrame) -> String {
559 let mut out = String::new();
560 out.push_str(&format!("{}\n", frame.n_atoms()));
561 out.push_str(&format!(
562 "Timestep {} time={}\n",
563 frame.timestep, frame.time
564 ));
565 for (pos, atom_type) in frame.positions.iter().zip(frame.atom_types.iter()) {
566 out.push_str(&format!("{} {} {} {}\n", atom_type, pos[0], pos[1], pos[2]));
567 }
568 out
569 }
570 pub fn write_frames(frames: &[TrajectoryFrame]) -> String {
572 frames.iter().map(Self::write_frame).collect()
573 }
574}
575#[allow(dead_code)]
581pub struct TrajLammpsWriter;
582#[allow(dead_code)]
583impl TrajLammpsWriter {
584 pub fn write_dump_frame(frame: &TrajectoryFrame, box_lo: [f64; 3], box_hi: [f64; 3]) -> String {
589 let mut out = String::new();
590 out.push_str("ITEM: TIMESTEP\n");
591 out.push_str(&format!("{}\n", frame.timestep));
592 out.push_str("ITEM: NUMBER OF ATOMS\n");
593 out.push_str(&format!("{}\n", frame.n_atoms()));
594 out.push_str("ITEM: BOX BOUNDS pp pp pp\n");
595 for i in 0..3 {
596 out.push_str(&format!("{} {}\n", box_lo[i], box_hi[i]));
597 }
598 out.push_str("ITEM: ATOMS id type x y z\n");
599 for (i, (pos, atom_type)) in frame
600 .positions
601 .iter()
602 .zip(frame.atom_types.iter())
603 .enumerate()
604 {
605 out.push_str(&format!(
606 "{} {} {} {} {}\n",
607 i + 1,
608 atom_type,
609 pos[0],
610 pos[1],
611 pos[2]
612 ));
613 }
614 out
615 }
616}
617#[allow(dead_code)]
619pub struct TrajectoryConverter;
620#[allow(dead_code)]
621impl TrajectoryConverter {
622 pub fn to_flat_xyz(frame: &TrajectoryFrame) -> Vec<f64> {
624 let mut out = Vec::with_capacity(frame.n_atoms() * 3);
625 for pos in &frame.positions {
626 out.extend_from_slice(pos);
627 }
628 out
629 }
630 pub fn from_flat_xyz(
632 flat: &[f64],
633 atom_types: Vec<String>,
634 timestep: u64,
635 time: f64,
636 ) -> std::result::Result<TrajectoryFrame, crate::Error> {
637 if !flat.len().is_multiple_of(3) {
638 return Err(crate::Error::Parse(format!(
639 "from_flat_xyz: length {} not divisible by 3",
640 flat.len()
641 )));
642 }
643 let n_atoms = flat.len() / 3;
644 if atom_types.len() != n_atoms {
645 return Err(crate::Error::Parse(format!(
646 "from_flat_xyz: {} atom types but {} positions",
647 atom_types.len(),
648 n_atoms
649 )));
650 }
651 let positions: Vec<[f64; 3]> = flat.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
652 Ok(TrajectoryFrame::new(timestep, time, positions, atom_types))
653 }
654 pub fn frame_displacement(
658 frame_a: &TrajectoryFrame,
659 frame_b: &TrajectoryFrame,
660 ) -> Vec<[f64; 3]> {
661 let n = frame_a.n_atoms().min(frame_b.n_atoms());
662 (0..n)
663 .map(|i| {
664 [
665 frame_b.positions[i][0] - frame_a.positions[i][0],
666 frame_b.positions[i][1] - frame_a.positions[i][1],
667 frame_b.positions[i][2] - frame_a.positions[i][2],
668 ]
669 })
670 .collect()
671 }
672 pub fn translate(frame: &TrajectoryFrame, delta: [f64; 3]) -> TrajectoryFrame {
674 let positions = frame
675 .positions
676 .iter()
677 .map(|p| [p[0] + delta[0], p[1] + delta[1], p[2] + delta[2]])
678 .collect();
679 TrajectoryFrame::new(
680 frame.timestep,
681 frame.time,
682 positions,
683 frame.atom_types.clone(),
684 )
685 }
686 pub fn scale(frame: &TrajectoryFrame, factor: f64) -> TrajectoryFrame {
688 let positions = frame
689 .positions
690 .iter()
691 .map(|p| [p[0] * factor, p[1] * factor, p[2] * factor])
692 .collect();
693 TrajectoryFrame::new(
694 frame.timestep,
695 frame.time,
696 positions,
697 frame.atom_types.clone(),
698 )
699 }
700}
701#[allow(dead_code)]
703pub struct TrajectoryConcatenator;
704#[allow(dead_code)]
705impl TrajectoryConcatenator {
706 pub fn concatenate(trajectories: &[Vec<TrajectoryFrame>]) -> Vec<TrajectoryFrame> {
708 let mut result = Vec::new();
709 let mut time_offset = 0.0_f64;
710 let mut step_offset = 0_u64;
711 for traj in trajectories {
712 for frame in traj {
713 let mut new_frame = frame.clone();
714 new_frame.time += time_offset;
715 new_frame.timestep += step_offset;
716 result.push(new_frame);
717 }
718 if let Some(last) = traj.last() {
719 time_offset += last.time;
720 step_offset += last.timestep + 1;
721 }
722 }
723 result
724 }
725 pub fn merge_sorted(trajectories: &[Vec<TrajectoryFrame>]) -> Vec<TrajectoryFrame> {
727 let mut all: Vec<&TrajectoryFrame> = trajectories.iter().flat_map(|t| t.iter()).collect();
728 all.sort_by(|a, b| {
729 a.time
730 .partial_cmp(&b.time)
731 .unwrap_or(std::cmp::Ordering::Equal)
732 });
733 all.into_iter().cloned().collect()
734 }
735}
736pub struct TrajectoryWriter {
739 pub(super) frames: Vec<(f64, Vec<Vec3>)>,
740}
741impl TrajectoryWriter {
742 pub fn new() -> Self {
744 Self { frames: Vec::new() }
745 }
746 pub fn add_frame(&mut self, time: f64, positions: &[Vec3]) {
751 self.frames.push((time, positions.to_vec()));
752 }
753 pub fn frame_count(&self) -> usize {
755 self.frames.len()
756 }
757 pub fn write_xdmf<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
759 let steps: Vec<(f64, &[Vec3])> = self
760 .frames
761 .iter()
762 .map(|(t, positions)| (*t, positions.as_slice()))
763 .collect();
764 xdmf::write_xdmf_temporal(writer, &steps)
765 }
766 pub fn write_xyz<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
777 for (i, (time, positions)) in self.frames.iter().enumerate() {
778 writeln!(writer, "{}", positions.len())?;
779 writeln!(writer, "Frame {} time={}", i, time)?;
780 for p in positions {
781 writeln!(writer, "X {} {} {}", p.x, p.y, p.z)?;
782 }
783 }
784 Ok(())
785 }
786}
787#[allow(dead_code)]
789pub struct Trajectory;
790#[allow(dead_code)]
791impl Trajectory {
792 pub fn compute_rmsd_trajectory(
805 frames: &[TrajectoryFrame],
806 reference: &TrajectoryFrame,
807 ) -> Vec<f64> {
808 let n_ref = reference.n_atoms();
809 frames
810 .iter()
811 .map(|frame| {
812 let n = frame.n_atoms();
813 if n != n_ref || n == 0 {
814 return f64::NAN;
815 }
816 let msd: f64 = frame
817 .positions
818 .iter()
819 .zip(reference.positions.iter())
820 .map(|(p, r)| {
821 let dx = p[0] - r[0];
822 let dy = p[1] - r[1];
823 let dz = p[2] - r[2];
824 dx * dx + dy * dy + dz * dz
825 })
826 .sum::<f64>()
827 / n as f64;
828 msd.sqrt()
829 })
830 .collect()
831 }
832 pub fn compute_radius_of_gyration(frames: &[TrajectoryFrame]) -> Vec<f64> {
844 frames
845 .iter()
846 .map(|frame| {
847 let n = frame.n_atoms();
848 if n == 0 {
849 return 0.0;
850 }
851 let inv_n = 1.0 / n as f64;
852 let com: [f64; 3] = {
853 let mut s = [0.0_f64; 3];
854 for p in &frame.positions {
855 s[0] += p[0];
856 s[1] += p[1];
857 s[2] += p[2];
858 }
859 [s[0] * inv_n, s[1] * inv_n, s[2] * inv_n]
860 };
861 let rg2 = frame
862 .positions
863 .iter()
864 .map(|p| {
865 let dx = p[0] - com[0];
866 let dy = p[1] - com[1];
867 let dz = p[2] - com[2];
868 dx * dx + dy * dy + dz * dz
869 })
870 .sum::<f64>()
871 * inv_n;
872 rg2.sqrt()
873 })
874 .collect()
875 }
876 pub fn align_to_reference(
887 frames: &[TrajectoryFrame],
888 reference: &TrajectoryFrame,
889 ) -> Vec<TrajectoryFrame> {
890 let n_ref = reference.n_atoms();
891 let ref_com = Self::centre_of_mass(&reference.positions);
892 let ref_centred: Vec<[f64; 3]> = reference
893 .positions
894 .iter()
895 .map(|p| [p[0] - ref_com[0], p[1] - ref_com[1], p[2] - ref_com[2]])
896 .collect();
897 frames
898 .iter()
899 .map(|frame| {
900 if frame.n_atoms() != n_ref {
901 return frame.clone();
902 }
903 let com = Self::centre_of_mass(&frame.positions);
904 let centred: Vec<[f64; 3]> = frame
905 .positions
906 .iter()
907 .map(|p| [p[0] - com[0], p[1] - com[1], p[2] - com[2]])
908 .collect();
909 let mut h = [[0.0_f64; 3]; 3];
910 for (c, r) in centred.iter().zip(ref_centred.iter()) {
911 for i in 0..3 {
912 for j in 0..3 {
913 h[i][j] += c[i] * r[j];
914 }
915 }
916 }
917 let rot = polar_rotation_3x3(h);
918 let positions: Vec<[f64; 3]> = centred
919 .iter()
920 .map(|c| {
921 let x = rot[0][0] * c[0] + rot[0][1] * c[1] + rot[0][2] * c[2] + ref_com[0];
922 let y = rot[1][0] * c[0] + rot[1][1] * c[1] + rot[1][2] * c[2] + ref_com[1];
923 let z = rot[2][0] * c[0] + rot[2][1] * c[1] + rot[2][2] * c[2] + ref_com[2];
924 [x, y, z]
925 })
926 .collect();
927 TrajectoryFrame::new(
928 frame.timestep,
929 frame.time,
930 positions,
931 frame.atom_types.clone(),
932 )
933 })
934 .collect()
935 }
936 fn centre_of_mass(positions: &[[f64; 3]]) -> [f64; 3] {
937 let n = positions.len();
938 if n == 0 {
939 return [0.0; 3];
940 }
941 let inv_n = 1.0 / n as f64;
942 let mut s = [0.0_f64; 3];
943 for p in positions {
944 s[0] += p[0];
945 s[1] += p[1];
946 s[2] += p[2];
947 }
948 [s[0] * inv_n, s[1] * inv_n, s[2] * inv_n]
949 }
950}
951#[allow(dead_code)]
953pub struct TrajectoryFilter;
954#[allow(dead_code)]
955impl TrajectoryFilter {
956 pub fn time_range(
958 frames: &[TrajectoryFrame],
959 t_start: f64,
960 t_end: f64,
961 ) -> Vec<TrajectoryFrame> {
962 frames
963 .iter()
964 .filter(|f| f.time >= t_start && f.time <= t_end)
965 .cloned()
966 .collect()
967 }
968 pub fn filter_atom_types(
970 frames: &[TrajectoryFrame],
971 keep_types: &[&str],
972 ) -> Vec<TrajectoryFrame> {
973 frames
974 .iter()
975 .map(|frame| {
976 let mut new_positions = Vec::new();
977 let mut new_types = Vec::new();
978 for (pos, atype) in frame.positions.iter().zip(frame.atom_types.iter()) {
979 if keep_types.iter().any(|&t| t == atype) {
980 new_positions.push(*pos);
981 new_types.push(atype.clone());
982 }
983 }
984 TrajectoryFrame {
985 timestep: frame.timestep,
986 time: frame.time,
987 positions: new_positions,
988 atom_types: new_types,
989 }
990 })
991 .collect()
992 }
993 pub fn remove_empty(frames: &[TrajectoryFrame]) -> Vec<TrajectoryFrame> {
995 frames
996 .iter()
997 .filter(|f| !f.positions.is_empty())
998 .cloned()
999 .collect()
1000 }
1001}
1002#[allow(dead_code)]
1004#[derive(Debug, Clone)]
1005pub struct VelocityFrame {
1006 pub timestep: u64,
1008 pub time: f64,
1010 pub velocities: Vec<[f64; 3]>,
1012}
1013#[allow(dead_code)]
1014impl VelocityFrame {
1015 pub fn new(timestep: u64, time: f64, velocities: Vec<[f64; 3]>) -> Self {
1017 Self {
1018 timestep,
1019 time,
1020 velocities,
1021 }
1022 }
1023 pub fn n_atoms(&self) -> usize {
1025 self.velocities.len()
1026 }
1027}
1028#[allow(dead_code)]
1030pub struct PeriodicImageHandler;
1031#[allow(dead_code)]
1032impl PeriodicImageHandler {
1033 pub fn wrap_positions(positions: &mut [[f64; 3]], box_size: [f64; 3]) {
1035 for pos in positions.iter_mut() {
1036 for d in 0..3 {
1037 if box_size[d] > 1e-30 {
1038 pos[d] = pos[d] - (pos[d] / box_size[d]).floor() * box_size[d];
1039 }
1040 }
1041 }
1042 }
1043 pub fn minimum_image_distance(a: [f64; 3], b: [f64; 3], box_size: [f64; 3]) -> f64 {
1045 let mut r2 = 0.0_f64;
1046 for d in 0..3 {
1047 let mut dx = b[d] - a[d];
1048 if box_size[d] > 1e-30 {
1049 dx -= (dx / box_size[d]).round() * box_size[d];
1050 }
1051 r2 += dx * dx;
1052 }
1053 r2.sqrt()
1054 }
1055 pub fn unwrap_trajectory(frames: &mut [TrajectoryFrame], box_size: [f64; 3]) {
1060 if frames.len() < 2 {
1061 return;
1062 }
1063 let n_atoms = frames[0].n_atoms();
1064 for i in 1..frames.len() {
1065 let n = n_atoms.min(frames[i].n_atoms());
1066 for j in 0..n {
1067 for d in 0..3 {
1068 if box_size[d] > 1e-30 {
1069 let dx = frames[i].positions[j][d] - frames[i - 1].positions[j][d];
1070 let shift = (dx / box_size[d]).round() * box_size[d];
1071 frames[i].positions[j][d] -= shift;
1072 }
1073 }
1074 }
1075 }
1076 }
1077}