1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
19
20const XTC_MAGIC: u32 = 0x5854_4300;
26
27pub struct XtcFrame {
29 pub step: i32,
31 pub time: f32,
33 pub box_matrix: [[f32; 3]; 3],
35 pub positions: Vec<[f32; 3]>,
37}
38
39impl XtcFrame {
40 pub fn n_atoms(&self) -> usize {
42 self.positions.len()
43 }
44
45 pub fn center_of_geometry(&self) -> [f32; 3] {
47 if self.positions.is_empty() {
48 return [0.0; 3];
49 }
50 let n = self.positions.len() as f32;
51 let mut cx = 0.0f32;
52 let mut cy = 0.0f32;
53 let mut cz = 0.0f32;
54 for p in &self.positions {
55 cx += p[0];
56 cy += p[1];
57 cz += p[2];
58 }
59 [cx / n, cy / n, cz / n]
60 }
61
62 pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
64 if self.positions.is_empty() {
65 return ([0.0; 3], [0.0; 3]);
66 }
67 let mut min = self.positions[0];
68 let mut max = self.positions[0];
69 for p in &self.positions {
70 for i in 0..3 {
71 if p[i] < min[i] {
72 min[i] = p[i];
73 }
74 if p[i] > max[i] {
75 max[i] = p[i];
76 }
77 }
78 }
79 (min, max)
80 }
81
82 pub fn box_volume(&self) -> f32 {
84 self.box_matrix[0][0] * self.box_matrix[1][1] * self.box_matrix[2][2]
85 }
86}
87
88pub struct SimpleXtcWriter {
90 pub frames: Vec<XtcFrame>,
92}
93
94impl SimpleXtcWriter {
95 pub fn new() -> Self {
97 Self { frames: Vec::new() }
98 }
99
100 pub fn add_frame(&mut self, step: i32, time: f32, positions: Vec<[f32; 3]>) {
102 let box_matrix = [[3.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 3.0]];
103 self.frames.push(XtcFrame {
104 step,
105 time,
106 box_matrix,
107 positions,
108 });
109 }
110
111 pub fn add_frame_with_box(
113 &mut self,
114 step: i32,
115 time: f32,
116 positions: Vec<[f32; 3]>,
117 box_matrix: [[f32; 3]; 3],
118 ) {
119 self.frames.push(XtcFrame {
120 step,
121 time,
122 box_matrix,
123 positions,
124 });
125 }
126
127 pub fn frame_count(&self) -> usize {
129 self.frames.len()
130 }
131
132 pub fn to_bytes(&self) -> Vec<u8> {
146 let mut buf = Vec::new();
147
148 buf.extend_from_slice(&XTC_MAGIC.to_le_bytes());
149 buf.extend_from_slice(&(self.frames.len() as u32).to_le_bytes());
150
151 for frame in &self.frames {
152 buf.extend_from_slice(&frame.step.to_le_bytes());
153 buf.extend_from_slice(&frame.time.to_le_bytes());
154 buf.extend_from_slice(&(frame.positions.len() as u32).to_le_bytes());
155
156 for row in &frame.box_matrix {
157 for &v in row {
158 buf.extend_from_slice(&v.to_le_bytes());
159 }
160 }
161
162 for pos in &frame.positions {
163 for &v in pos {
164 buf.extend_from_slice(&v.to_le_bytes());
165 }
166 }
167 }
168
169 buf
170 }
171}
172
173impl Default for SimpleXtcWriter {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179pub struct SimpleXtcReader;
181
182impl SimpleXtcReader {
183 pub fn from_bytes(data: &[u8]) -> Result<Vec<XtcFrame>, String> {
185 let mut cur = 0usize;
186
187 macro_rules! read_u32 {
188 () => {{
189 if cur + 4 > data.len() {
190 return Err(format!("XTC: unexpected EOF at offset {cur}"));
191 }
192 let v = u32::from_le_bytes(
193 data[cur..cur + 4]
194 .try_into()
195 .expect("slice length must match"),
196 );
197 cur += 4;
198 v
199 }};
200 }
201 macro_rules! read_i32 {
202 () => {{
203 if cur + 4 > data.len() {
204 return Err(format!("XTC: unexpected EOF at offset {cur}"));
205 }
206 let v = i32::from_le_bytes(
207 data[cur..cur + 4]
208 .try_into()
209 .expect("slice length must match"),
210 );
211 cur += 4;
212 v
213 }};
214 }
215 macro_rules! read_f32 {
216 () => {{
217 if cur + 4 > data.len() {
218 return Err(format!("XTC: unexpected EOF at offset {cur}"));
219 }
220 let v = f32::from_le_bytes(
221 data[cur..cur + 4]
222 .try_into()
223 .expect("slice length must match"),
224 );
225 cur += 4;
226 v
227 }};
228 }
229
230 let magic = read_u32!();
231 if magic != XTC_MAGIC {
232 return Err(format!("XTC: bad magic 0x{magic:08X}"));
233 }
234
235 let n_frames = read_u32!() as usize;
236 let mut frames = Vec::with_capacity(n_frames);
237
238 for _ in 0..n_frames {
239 let step = read_i32!();
240 let time = read_f32!();
241 let n_atoms = read_u32!() as usize;
242
243 let mut box_matrix = [[0f32; 3]; 3];
244 for row in &mut box_matrix {
245 for v in row.iter_mut() {
246 *v = read_f32!();
247 }
248 }
249
250 let mut positions = Vec::with_capacity(n_atoms);
251 for _ in 0..n_atoms {
252 let x = read_f32!();
253 let y = read_f32!();
254 let z = read_f32!();
255 positions.push([x, y, z]);
256 }
257
258 frames.push(XtcFrame {
259 step,
260 time,
261 box_matrix,
262 positions,
263 });
264 }
265
266 Ok(frames)
267 }
268
269 pub fn read_frame_at(data: &[u8], frame_idx: usize) -> Result<XtcFrame, String> {
271 let frames = Self::from_bytes(data)?;
272 if frame_idx >= frames.len() {
273 return Err(format!(
274 "frame index {frame_idx} out of range (total {})",
275 frames.len()
276 ));
277 }
278 let mut frames = frames;
281 Ok(frames.swap_remove(frame_idx.min(frames.len() - 1)))
282 }
283}
284
285pub struct TrajectoryMetadata {
287 pub n_frames: usize,
289 pub n_atoms: usize,
291 pub first_time: f32,
293 pub last_time: f32,
295 pub first_step: i32,
297 pub last_step: i32,
299}
300
301impl TrajectoryMetadata {
302 pub fn from_xtc_frames(frames: &[XtcFrame]) -> Option<Self> {
304 if frames.is_empty() {
305 return None;
306 }
307 Some(Self {
308 n_frames: frames.len(),
309 n_atoms: frames[0].positions.len(),
310 first_time: frames[0].time,
311 last_time: frames[frames.len() - 1].time,
312 first_step: frames[0].step,
313 last_step: frames[frames.len() - 1].step,
314 })
315 }
316
317 pub fn duration(&self) -> f32 {
319 self.last_time - self.first_time
320 }
321
322 pub fn avg_dt(&self) -> f32 {
324 if self.n_frames <= 1 {
325 return 0.0;
326 }
327 self.duration() / (self.n_frames - 1) as f32
328 }
329}
330
331pub struct CoordinateQuantizer {
340 pub precision: u32,
342 pub scale: f32,
344}
345
346impl CoordinateQuantizer {
347 pub fn new(precision: u32) -> Self {
349 let scale = 10.0_f32.powi(precision as i32);
350 Self { precision, scale }
351 }
352
353 pub fn quantize(&self, pos: [f32; 3]) -> [f32; 3] {
355 [
356 (pos[0] * self.scale).round() / self.scale,
357 (pos[1] * self.scale).round() / self.scale,
358 (pos[2] * self.scale).round() / self.scale,
359 ]
360 }
361
362 pub fn quantize_frame(&self, positions: &mut [[f32; 3]]) {
364 for p in positions.iter_mut() {
365 *p = self.quantize(*p);
366 }
367 }
368
369 pub fn max_error(&self) -> f32 {
371 0.5 / self.scale
372 }
373}
374
375pub struct DcdHeader {
381 pub n_atoms: u32,
383 pub n_frames: u32,
385 pub timestep: f32,
387 pub title: String,
389}
390
391impl DcdHeader {
392 pub fn duration(&self) -> f32 {
394 self.timestep * self.n_frames as f32
395 }
396}
397
398pub struct DcdFrame {
400 pub x: Vec<f32>,
402 pub y: Vec<f32>,
404 pub z: Vec<f32>,
406}
407
408impl DcdFrame {
409 pub fn n_atoms(&self) -> usize {
411 self.x.len()
412 }
413
414 pub fn position(&self, i: usize) -> [f32; 3] {
416 [self.x[i], self.y[i], self.z[i]]
417 }
418
419 pub fn to_interleaved(&self) -> Vec<[f32; 3]> {
421 (0..self.n_atoms()).map(|i| self.position(i)).collect()
422 }
423
424 pub fn center_of_geometry(&self) -> [f32; 3] {
426 if self.x.is_empty() {
427 return [0.0; 3];
428 }
429 let n = self.x.len() as f32;
430 let cx: f32 = self.x.iter().sum::<f32>() / n;
431 let cy: f32 = self.y.iter().sum::<f32>() / n;
432 let cz: f32 = self.z.iter().sum::<f32>() / n;
433 [cx, cy, cz]
434 }
435}
436
437pub struct SimpleDcdWriter {
439 pub header: DcdHeader,
441 pub frames: Vec<DcdFrame>,
443}
444
445impl SimpleDcdWriter {
446 pub fn new(n_atoms: u32, timestep: f32) -> Self {
448 let header = DcdHeader {
449 n_atoms,
450 n_frames: 0,
451 timestep,
452 title: String::from("OxiPhysics simplified DCD"),
453 };
454 Self {
455 header,
456 frames: Vec::new(),
457 }
458 }
459
460 pub fn with_title(n_atoms: u32, timestep: f32, title: &str) -> Self {
462 let header = DcdHeader {
463 n_atoms,
464 n_frames: 0,
465 timestep,
466 title: title.to_string(),
467 };
468 Self {
469 header,
470 frames: Vec::new(),
471 }
472 }
473
474 pub fn add_frame(&mut self, positions: &[[f32; 3]]) {
476 let x: Vec<f32> = positions.iter().map(|p| p[0]).collect();
477 let y: Vec<f32> = positions.iter().map(|p| p[1]).collect();
478 let z: Vec<f32> = positions.iter().map(|p| p[2]).collect();
479 self.frames.push(DcdFrame { x, y, z });
480 self.header.n_frames = self.frames.len() as u32;
481 }
482
483 pub fn add_frame_xyz(&mut self, x: Vec<f32>, y: Vec<f32>, z: Vec<f32>) {
485 self.frames.push(DcdFrame { x, y, z });
486 self.header.n_frames = self.frames.len() as u32;
487 }
488
489 pub fn frame_count(&self) -> usize {
491 self.frames.len()
492 }
493
494 pub fn to_bytes(&self) -> Vec<u8> {
516 let mut buf = Vec::new();
517
518 let hdr_payload_len: u32 = 4 + 4 + 4 + 4 + 80;
521 buf.extend_from_slice(&hdr_payload_len.to_le_bytes());
522 buf.extend_from_slice(b"CORD");
523 buf.extend_from_slice(&self.header.n_frames.to_le_bytes());
524 buf.extend_from_slice(&self.header.n_atoms.to_le_bytes());
525 buf.extend_from_slice(&self.header.timestep.to_le_bytes());
526
527 let mut title_bytes = [b' '; 80];
529 let src = self.header.title.as_bytes();
530 let copy_len = src.len().min(80);
531 title_bytes[..copy_len].copy_from_slice(&src[..copy_len]);
532 buf.extend_from_slice(&title_bytes);
533
534 buf.extend_from_slice(&hdr_payload_len.to_le_bytes()); let coord_block_len = self.header.n_atoms * 4; for frame in &self.frames {
540 for coords in [&frame.x, &frame.y, &frame.z] {
541 buf.extend_from_slice(&coord_block_len.to_le_bytes());
542 for &v in coords.iter() {
543 buf.extend_from_slice(&v.to_le_bytes());
544 }
545 buf.extend_from_slice(&coord_block_len.to_le_bytes());
546 }
547 }
548
549 buf
550 }
551}
552
553pub struct SimpleDcdReader;
555
556impl SimpleDcdReader {
557 pub fn from_bytes(data: &[u8]) -> Result<(DcdHeader, Vec<DcdFrame>), String> {
559 let mut cur = 0usize;
560
561 macro_rules! need {
562 ($n:expr) => {
563 if cur + $n > data.len() {
564 return Err(format!("DCD: unexpected EOF at offset {cur}"));
565 }
566 };
567 }
568 macro_rules! read_u32 {
569 () => {{
570 need!(4);
571 let v = u32::from_le_bytes(
572 data[cur..cur + 4]
573 .try_into()
574 .expect("slice length must match"),
575 );
576 cur += 4;
577 v
578 }};
579 }
580 macro_rules! read_f32 {
581 () => {{
582 need!(4);
583 let v = f32::from_le_bytes(
584 data[cur..cur + 4]
585 .try_into()
586 .expect("slice length must match"),
587 );
588 cur += 4;
589 v
590 }};
591 }
592
593 let hdr_len = read_u32!() as usize;
595 if hdr_len < 4 + 4 + 4 + 4 + 80 {
596 return Err(format!("DCD: header block too short ({hdr_len})"));
597 }
598
599 need!(4);
600 let sig = &data[cur..cur + 4];
601 if sig != b"CORD" {
602 return Err("DCD: expected 'CORD' signature".to_string());
603 }
604 cur += 4;
605
606 let n_frames = read_u32!();
607 let n_atoms = read_u32!();
608 let timestep = read_f32!();
609
610 need!(80);
611 let title_raw = &data[cur..cur + 80];
612 cur += 80;
613 let title = String::from_utf8_lossy(title_raw).trim_end().to_string();
614
615 let hdr_len2 = read_u32!();
617 if hdr_len2 as usize != hdr_len {
618 return Err(format!(
619 "DCD: header closing length mismatch ({hdr_len2} vs {hdr_len})"
620 ));
621 }
622
623 let header = DcdHeader {
624 n_atoms,
625 n_frames,
626 timestep,
627 title,
628 };
629 let n = n_atoms as usize;
630
631 let mut frames = Vec::with_capacity(n_frames as usize);
633
634 for frame_idx in 0..n_frames {
635 let mut xyz: [Vec<f32>; 3] = [
636 Vec::with_capacity(n),
637 Vec::with_capacity(n),
638 Vec::with_capacity(n),
639 ];
640
641 for dim in 0..3usize {
642 let block_len = read_u32!() as usize;
643 let expected = n * 4;
644 if block_len != expected {
645 return Err(format!(
646 "DCD: frame {frame_idx} dim {dim}: block length {block_len} != {expected}"
647 ));
648 }
649 for _ in 0..n {
650 xyz[dim].push(read_f32!());
651 }
652 let block_len2 = read_u32!() as usize;
653 if block_len2 != block_len {
654 return Err(format!(
655 "DCD: frame {frame_idx} dim {dim}: closing block length mismatch"
656 ));
657 }
658 }
659
660 let [x, y, z] = xyz;
661 frames.push(DcdFrame { x, y, z });
662 }
663
664 Ok((header, frames))
665 }
666
667 pub fn read_metadata(data: &[u8]) -> Result<DcdHeader, String> {
669 let (header, _) = Self::from_bytes(data)?;
670 Ok(header)
671 }
672}
673
674#[allow(dead_code)]
683pub fn write_dcd_trajectory(n_atoms: u32, timestep: f32, frames: &[Vec<[f32; 3]>]) -> Vec<u8> {
684 let mut writer = SimpleDcdWriter::new(n_atoms, timestep);
685 for frame in frames {
686 writer.add_frame(frame);
687 }
688 writer.to_bytes()
689}
690
691#[allow(dead_code)]
696pub fn append_dcd_frame(existing: &[u8], new_positions: &[[f32; 3]]) -> Result<Vec<u8>, String> {
697 let (header, mut frames) = SimpleDcdReader::from_bytes(existing)?;
698 let mut writer = SimpleDcdWriter::with_title(header.n_atoms, header.timestep, &header.title);
699 for frame in &frames {
700 writer.add_frame_xyz(frame.x.clone(), frame.y.clone(), frame.z.clone());
701 }
702 let new_frame: Vec<[f32; 3]> = new_positions.to_vec();
704 writer.add_frame(&new_frame);
705 frames.clear();
707 Ok(writer.to_bytes())
708}
709
710#[allow(dead_code)]
719pub fn apply_xtc_precision(writer: &mut SimpleXtcWriter, precision: u32) {
720 let q = CoordinateQuantizer::new(precision);
721 for frame in writer.frames.iter_mut() {
722 q.quantize_frame(&mut frame.positions);
723 }
724}
725
726#[allow(dead_code)]
730pub fn xtc_recompress(data: &[u8], precision: u32) -> Result<Vec<u8>, String> {
731 let frames = SimpleXtcReader::from_bytes(data)?;
732 let q = CoordinateQuantizer::new(precision);
733 let mut writer = SimpleXtcWriter::new();
734 for mut frame in frames {
735 q.quantize_frame(&mut frame.positions);
736 writer.add_frame_with_box(frame.step, frame.time, frame.positions, frame.box_matrix);
737 }
738 Ok(writer.to_bytes())
739}
740
741#[allow(dead_code)]
743pub fn compute_rmsd(a: &[[f32; 3]], b: &[[f32; 3]]) -> f32 {
744 if a.is_empty() || a.len() != b.len() {
745 return 0.0;
746 }
747 let n = a.len() as f32;
748 let sum_sq: f32 = a
749 .iter()
750 .zip(b.iter())
751 .map(|(ai, bi)| {
752 let dx = ai[0] - bi[0];
753 let dy = ai[1] - bi[1];
754 let dz = ai[2] - bi[2];
755 dx * dx + dy * dy + dz * dz
756 })
757 .sum();
758 (sum_sq / n).sqrt()
759}
760
761#[allow(dead_code)]
771pub fn xtc_to_dcd(xtc_data: &[u8]) -> Result<Vec<u8>, String> {
772 let frames = SimpleXtcReader::from_bytes(xtc_data)?;
773 if frames.is_empty() {
774 return Ok(SimpleDcdWriter::new(0, 0.0).to_bytes());
775 }
776 let n_atoms = frames[0].n_atoms() as u32;
777
778 let meta = TrajectoryMetadata::from_xtc_frames(&frames).expect("value should be present");
780 let timestep = meta.avg_dt();
781
782 let mut writer = SimpleDcdWriter::with_title(n_atoms, timestep, "Converted from XTC");
783 for frame in &frames {
784 let interleaved = frame.positions.clone();
785 writer.add_frame(&interleaved);
786 }
787 Ok(writer.to_bytes())
788}
789
790#[allow(dead_code)]
794pub fn dcd_to_xtc(dcd_data: &[u8]) -> Result<Vec<u8>, String> {
795 let (header, frames) = SimpleDcdReader::from_bytes(dcd_data)?;
796 let timestep_ps = header.timestep;
797 let mut writer = SimpleXtcWriter::new();
798 for (idx, frame) in frames.iter().enumerate() {
799 let positions_nm: Vec<[f32; 3]> = (0..frame.n_atoms())
800 .map(|i| [frame.x[i] / 10.0, frame.y[i] / 10.0, frame.z[i] / 10.0])
801 .collect();
802 let time_ps = timestep_ps * idx as f32;
803 writer.add_frame(idx as i32, time_ps, positions_nm);
804 }
805 Ok(writer.to_bytes())
806}
807
808#[allow(dead_code)]
814pub fn xtc_extract_frame_times(data: &[u8]) -> Result<Vec<f32>, String> {
815 let frames = SimpleXtcReader::from_bytes(data)?;
816 Ok(frames.iter().map(|f| f.time).collect())
817}
818
819#[allow(dead_code)]
821pub fn xtc_extract_frame_steps(data: &[u8]) -> Result<Vec<i32>, String> {
822 let frames = SimpleXtcReader::from_bytes(data)?;
823 Ok(frames.iter().map(|f| f.step).collect())
824}
825
826#[allow(dead_code)]
828pub fn xtc_frame_time_deltas(data: &[u8]) -> Result<Vec<f32>, String> {
829 let times = xtc_extract_frame_times(data)?;
830 if times.len() < 2 {
831 return Ok(Vec::new());
832 }
833 Ok(times.windows(2).map(|w| w[1] - w[0]).collect())
834}
835
836pub struct XtcFrameWithVelocity {
842 pub frame: XtcFrame,
844 pub velocities: Vec<[f32; 3]>,
846}
847
848impl XtcFrameWithVelocity {
849 #[allow(dead_code)]
851 pub fn new(frame: XtcFrame, velocities: Vec<[f32; 3]>) -> Self {
852 Self { frame, velocities }
853 }
854
855 #[allow(dead_code)]
857 pub fn kinetic_energy_proxy(&self) -> f32 {
858 self.velocities
859 .iter()
860 .map(|v| 0.5 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]))
861 .sum()
862 }
863
864 #[allow(dead_code)]
866 pub fn rms_speed(&self) -> f32 {
867 if self.velocities.is_empty() {
868 return 0.0;
869 }
870 let n = self.velocities.len() as f32;
871 let sum_sq: f32 = self
872 .velocities
873 .iter()
874 .map(|v| v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
875 .sum();
876 (sum_sq / n).sqrt()
877 }
878
879 #[allow(dead_code)]
881 pub fn max_speed(&self) -> f32 {
882 self.velocities
883 .iter()
884 .map(|v| (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt())
885 .fold(0.0f32, f32::max)
886 }
887}
888
889pub struct XtcVelocityWriter {
894 pub positions: SimpleXtcWriter,
896 pub velocities: Vec<Vec<[f32; 3]>>,
898}
899
900impl XtcVelocityWriter {
901 #[allow(dead_code)]
903 pub fn new() -> Self {
904 Self {
905 positions: SimpleXtcWriter::new(),
906 velocities: Vec::new(),
907 }
908 }
909
910 #[allow(dead_code)]
912 pub fn add_frame(
913 &mut self,
914 step: i32,
915 time: f32,
916 positions: Vec<[f32; 3]>,
917 velocities: Vec<[f32; 3]>,
918 ) {
919 self.positions.add_frame(step, time, positions);
920 self.velocities.push(velocities);
921 }
922
923 #[allow(dead_code)]
925 pub fn frame_count(&self) -> usize {
926 self.positions.frame_count()
927 }
928
929 #[allow(dead_code)]
933 pub fn to_bytes(&self) -> Vec<u8> {
934 let mut buf = self.positions.to_bytes();
935 buf.extend_from_slice(b"OXVL");
937 buf.extend_from_slice(&(self.velocities.len() as u32).to_le_bytes());
938 for frame_vels in &self.velocities {
939 buf.extend_from_slice(&(frame_vels.len() as u32).to_le_bytes());
940 for v in frame_vels {
941 for &c in v {
942 buf.extend_from_slice(&c.to_le_bytes());
943 }
944 }
945 }
946 buf
947 }
948
949 #[allow(dead_code)]
951 pub fn frame_kinetic_energy(&self, frame_idx: usize) -> f32 {
952 if frame_idx >= self.velocities.len() {
953 return 0.0;
954 }
955 self.velocities[frame_idx]
956 .iter()
957 .map(|v| 0.5 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]))
958 .sum()
959 }
960}
961
962impl Default for XtcVelocityWriter {
963 fn default() -> Self {
964 Self::new()
965 }
966}
967
968#[allow(dead_code)]
973pub fn compute_finite_difference_velocities(frames: &[XtcFrame], dt_ps: f32) -> Vec<Vec<[f32; 3]>> {
974 if frames.len() < 2 || dt_ps < 1e-30 {
975 return Vec::new();
976 }
977 frames
978 .windows(2)
979 .map(|w| {
980 let n = w[0].positions.len().min(w[1].positions.len());
981 (0..n)
982 .map(|i| {
983 let p0 = w[0].positions[i];
984 let p1 = w[1].positions[i];
985 [
986 (p1[0] - p0[0]) / dt_ps,
987 (p1[1] - p0[1]) / dt_ps,
988 (p1[2] - p0[2]) / dt_ps,
989 ]
990 })
991 .collect()
992 })
993 .collect()
994}
995
996#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 fn approx_eq(a: f32, b: f32) -> bool {
1005 (a - b).abs() < 1e-5
1006 }
1007
1008 #[test]
1011 fn xtc_empty_writer() {
1012 let w = SimpleXtcWriter::new();
1013 assert_eq!(w.frame_count(), 0);
1014 let bytes = w.to_bytes();
1015 assert_eq!(bytes.len(), 8);
1017 }
1018
1019 #[test]
1020 fn xtc_round_trip_3frames_5atoms() {
1021 let mut writer = SimpleXtcWriter::new();
1022
1023 let pos0: Vec<[f32; 3]> = (0..5)
1024 .map(|i| [i as f32 * 0.1, i as f32 * 0.2, i as f32 * 0.3])
1025 .collect();
1026 let pos1: Vec<[f32; 3]> = (0..5)
1027 .map(|i| [i as f32 * 0.4, i as f32 * 0.5, i as f32 * 0.6])
1028 .collect();
1029 let pos2: Vec<[f32; 3]> = (0..5)
1030 .map(|i| [i as f32 * 0.7, i as f32 * 0.8, i as f32 * 0.9])
1031 .collect();
1032
1033 writer.add_frame(0, 0.0, pos0.clone());
1034 writer.add_frame(1, 0.5, pos1.clone());
1035 writer.add_frame(2, 1.0, pos2.clone());
1036
1037 assert_eq!(writer.frame_count(), 3);
1038
1039 let bytes = writer.to_bytes();
1040 let frames = SimpleXtcReader::from_bytes(&bytes).expect("XTC parse failed");
1041
1042 assert_eq!(frames.len(), 3);
1043
1044 for (f, original) in frames.iter().zip([&pos0, &pos1, &pos2]) {
1045 assert_eq!(f.positions.len(), 5);
1046 for (got, exp) in f.positions.iter().zip(original.iter()) {
1047 for k in 0..3 {
1048 assert!(
1049 approx_eq(got[k], exp[k]),
1050 "mismatch: got {} exp {}",
1051 got[k],
1052 exp[k]
1053 );
1054 }
1055 }
1056 }
1057
1058 assert_eq!(frames[0].step, 0);
1059 assert!(approx_eq(frames[1].time, 0.5));
1060 assert_eq!(frames[2].step, 2);
1061 }
1062
1063 #[test]
1064 fn xtc_frame_count_check() {
1065 let mut w = SimpleXtcWriter::new();
1066 for i in 0..10 {
1067 w.add_frame(i, i as f32 * 0.1, vec![[0.0; 3]]);
1068 }
1069 assert_eq!(w.frame_count(), 10);
1070 }
1071
1072 #[test]
1075 fn xtc_frame_center_of_geometry() {
1076 let frame = XtcFrame {
1077 step: 0,
1078 time: 0.0,
1079 box_matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1080 positions: vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]],
1081 };
1082 let cog = frame.center_of_geometry();
1083 assert!(approx_eq(cog[0], 2.0 / 3.0));
1084 assert!(approx_eq(cog[1], 2.0 / 3.0));
1085 assert!(approx_eq(cog[2], 0.0));
1086 }
1087
1088 #[test]
1089 fn xtc_frame_bounding_box() {
1090 let frame = XtcFrame {
1091 step: 0,
1092 time: 0.0,
1093 box_matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
1094 positions: vec![[-1.0, 2.0, 0.0], [3.0, -1.0, 5.0]],
1095 };
1096 let (min, max) = frame.bounding_box();
1097 assert!(approx_eq(min[0], -1.0));
1098 assert!(approx_eq(max[0], 3.0));
1099 assert!(approx_eq(min[1], -1.0));
1100 assert!(approx_eq(max[1], 2.0));
1101 }
1102
1103 #[test]
1104 fn xtc_frame_box_volume() {
1105 let frame = XtcFrame {
1106 step: 0,
1107 time: 0.0,
1108 box_matrix: [[3.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 5.0]],
1109 positions: vec![],
1110 };
1111 assert!(approx_eq(frame.box_volume(), 60.0));
1112 }
1113
1114 #[test]
1117 fn xtc_read_frame_at() {
1118 let mut writer = SimpleXtcWriter::new();
1119 writer.add_frame(0, 0.0, vec![[1.0, 0.0, 0.0]]);
1120 writer.add_frame(1, 0.5, vec![[2.0, 0.0, 0.0]]);
1121 writer.add_frame(2, 1.0, vec![[3.0, 0.0, 0.0]]);
1122
1123 let bytes = writer.to_bytes();
1124 let frame = SimpleXtcReader::read_frame_at(&bytes, 1).unwrap();
1125 assert_eq!(frame.step, 1);
1126 }
1127
1128 #[test]
1129 fn xtc_read_frame_at_out_of_range() {
1130 let mut writer = SimpleXtcWriter::new();
1131 writer.add_frame(0, 0.0, vec![[0.0; 3]]);
1132 let bytes = writer.to_bytes();
1133 assert!(SimpleXtcReader::read_frame_at(&bytes, 5).is_err());
1134 }
1135
1136 #[test]
1139 fn trajectory_metadata_basic() {
1140 let mut writer = SimpleXtcWriter::new();
1141 writer.add_frame(0, 0.0, vec![[0.0; 3]; 10]);
1142 writer.add_frame(100, 10.0, vec![[0.0; 3]; 10]);
1143 writer.add_frame(200, 20.0, vec![[0.0; 3]; 10]);
1144
1145 let bytes = writer.to_bytes();
1146 let frames = SimpleXtcReader::from_bytes(&bytes).unwrap();
1147 let meta = TrajectoryMetadata::from_xtc_frames(&frames).unwrap();
1148
1149 assert_eq!(meta.n_frames, 3);
1150 assert_eq!(meta.n_atoms, 10);
1151 assert!(approx_eq(meta.first_time, 0.0));
1152 assert!(approx_eq(meta.last_time, 20.0));
1153 assert!(approx_eq(meta.duration(), 20.0));
1154 assert!(approx_eq(meta.avg_dt(), 10.0));
1155 }
1156
1157 #[test]
1158 fn trajectory_metadata_empty() {
1159 let frames: Vec<XtcFrame> = vec![];
1160 assert!(TrajectoryMetadata::from_xtc_frames(&frames).is_none());
1161 }
1162
1163 #[test]
1166 fn quantizer_precision_3() {
1167 let q = CoordinateQuantizer::new(3);
1168 let pos = [1.23456, 7.89012, -0.00050];
1169 let qp = q.quantize(pos);
1170 assert!(approx_eq(qp[0], 1.235));
1171 assert!(approx_eq(qp[1], 7.890));
1172 assert!(qp[2].abs() < 0.001 + 1e-6);
1174 }
1175
1176 #[test]
1177 fn quantizer_max_error() {
1178 let q = CoordinateQuantizer::new(3);
1179 assert!(approx_eq(q.max_error(), 0.0005));
1180 }
1181
1182 #[test]
1183 fn quantizer_frame() {
1184 let q = CoordinateQuantizer::new(2);
1185 let mut positions = vec![[1.234, 5.678, 9.012]];
1186 q.quantize_frame(&mut positions);
1187 assert!(approx_eq(positions[0][0], 1.23));
1188 assert!(approx_eq(positions[0][1], 5.68));
1189 }
1190
1191 #[test]
1194 fn xtc_custom_box_matrix() {
1195 let mut writer = SimpleXtcWriter::new();
1196 let custom_box = [[5.0, 0.0, 0.0], [0.0, 6.0, 0.0], [0.0, 0.0, 7.0]];
1197 writer.add_frame_with_box(0, 0.0, vec![[0.0; 3]], custom_box);
1198
1199 let bytes = writer.to_bytes();
1200 let frames = SimpleXtcReader::from_bytes(&bytes).unwrap();
1201 assert!(approx_eq(frames[0].box_matrix[0][0], 5.0));
1202 assert!(approx_eq(frames[0].box_matrix[1][1], 6.0));
1203 assert!(approx_eq(frames[0].box_matrix[2][2], 7.0));
1204 }
1205
1206 #[test]
1209 fn dcd_empty_writer() {
1210 let w = SimpleDcdWriter::new(4, 2.0);
1211 assert_eq!(w.frame_count(), 0);
1212 let bytes = w.to_bytes();
1214 assert!(!bytes.is_empty());
1215 }
1216
1217 #[test]
1218 fn dcd_round_trip_2frames_4atoms() {
1219 let mut writer = SimpleDcdWriter::new(4, 2.0);
1220
1221 let frame0: Vec<[f32; 3]> = vec![
1222 [1.0, 2.0, 3.0],
1223 [4.0, 5.0, 6.0],
1224 [7.0, 8.0, 9.0],
1225 [10.0, 11.0, 12.0],
1226 ];
1227 let frame1: Vec<[f32; 3]> = vec![
1228 [0.1, 0.2, 0.3],
1229 [0.4, 0.5, 0.6],
1230 [0.7, 0.8, 0.9],
1231 [1.0, 1.1, 1.2],
1232 ];
1233
1234 writer.add_frame(&frame0);
1235 writer.add_frame(&frame1);
1236
1237 assert_eq!(writer.frame_count(), 2);
1238 assert_eq!(writer.header.n_frames, 2);
1239
1240 let bytes = writer.to_bytes();
1241 let (header, frames) = SimpleDcdReader::from_bytes(&bytes).expect("DCD parse failed");
1242
1243 assert_eq!(header.n_atoms, 4);
1244 assert_eq!(header.n_frames, 2);
1245 assert!(approx_eq(header.timestep, 2.0));
1246 assert_eq!(frames.len(), 2);
1247
1248 let expected_x0: Vec<f32> = frame0.iter().map(|p| p[0]).collect();
1250 for (got, &exp) in frames[0].x.iter().zip(expected_x0.iter()) {
1251 assert!(approx_eq(*got, exp), "x mismatch: got {got} exp {exp}");
1252 }
1253
1254 for (i, pos) in frame1.iter().enumerate() {
1256 assert!(approx_eq(frames[1].x[i], pos[0]));
1257 assert!(approx_eq(frames[1].y[i], pos[1]));
1258 assert!(approx_eq(frames[1].z[i], pos[2]));
1259 }
1260 }
1261
1262 #[test]
1263 fn dcd_frame_count_check() {
1264 let mut w = SimpleDcdWriter::new(2, 1.0);
1265 for _ in 0..7 {
1266 w.add_frame(&[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]);
1267 }
1268 assert_eq!(w.frame_count(), 7);
1269 }
1270
1271 #[test]
1274 fn dcd_frame_to_interleaved() {
1275 let frame = DcdFrame {
1276 x: vec![1.0, 4.0],
1277 y: vec![2.0, 5.0],
1278 z: vec![3.0, 6.0],
1279 };
1280 let interleaved = frame.to_interleaved();
1281 assert_eq!(interleaved.len(), 2);
1282 assert!(approx_eq(interleaved[0][0], 1.0));
1283 assert!(approx_eq(interleaved[1][2], 6.0));
1284 }
1285
1286 #[test]
1287 fn dcd_frame_center_of_geometry() {
1288 let frame = DcdFrame {
1289 x: vec![0.0, 2.0],
1290 y: vec![0.0, 4.0],
1291 z: vec![0.0, 6.0],
1292 };
1293 let cog = frame.center_of_geometry();
1294 assert!(approx_eq(cog[0], 1.0));
1295 assert!(approx_eq(cog[1], 2.0));
1296 assert!(approx_eq(cog[2], 3.0));
1297 }
1298
1299 #[test]
1302 fn dcd_custom_title() {
1303 let mut writer = SimpleDcdWriter::with_title(2, 1.0, "Custom Title");
1304 writer.add_frame(&[[0.0; 3], [1.0; 3]]);
1305
1306 let bytes = writer.to_bytes();
1307 let (header, _) = SimpleDcdReader::from_bytes(&bytes).unwrap();
1308 assert!(header.title.contains("Custom Title"));
1309 }
1310
1311 #[test]
1314 fn dcd_add_frame_xyz() {
1315 let mut writer = SimpleDcdWriter::new(3, 1.0);
1316 writer.add_frame_xyz(
1317 vec![1.0, 2.0, 3.0],
1318 vec![4.0, 5.0, 6.0],
1319 vec![7.0, 8.0, 9.0],
1320 );
1321 assert_eq!(writer.frame_count(), 1);
1322
1323 let bytes = writer.to_bytes();
1324 let (_, frames) = SimpleDcdReader::from_bytes(&bytes).unwrap();
1325 assert_eq!(frames[0].x, vec![1.0, 2.0, 3.0]);
1326 }
1327
1328 #[test]
1331 fn dcd_header_duration() {
1332 let header = DcdHeader {
1333 n_atoms: 10,
1334 n_frames: 100,
1335 timestep: 2.0,
1336 title: String::new(),
1337 };
1338 assert!(approx_eq(header.duration(), 200.0));
1339 }
1340
1341 #[test]
1344 fn dcd_read_metadata() {
1345 let mut writer = SimpleDcdWriter::new(5, 3.0);
1346 writer.add_frame(&[[0.0; 3]; 5]);
1347 writer.add_frame(&[[1.0; 3]; 5]);
1348
1349 let bytes = writer.to_bytes();
1350 let header = SimpleDcdReader::read_metadata(&bytes).unwrap();
1351 assert_eq!(header.n_atoms, 5);
1352 assert_eq!(header.n_frames, 2);
1353 assert!(approx_eq(header.timestep, 3.0));
1354 }
1355
1356 #[test]
1359 fn write_dcd_trajectory_basic() {
1360 let f0: Vec<[f32; 3]> = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1361 let f1: Vec<[f32; 3]> = vec![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]];
1362 let bytes = write_dcd_trajectory(2, 2.0, &[f0, f1]);
1363 let (header, frames) = SimpleDcdReader::from_bytes(&bytes).unwrap();
1364 assert_eq!(header.n_atoms, 2);
1365 assert_eq!(header.n_frames, 2);
1366 assert!(approx_eq(frames[0].x[0], 1.0));
1367 assert!(approx_eq(frames[1].z[1], 12.0));
1368 }
1369
1370 #[test]
1371 fn write_dcd_trajectory_empty() {
1372 let bytes = write_dcd_trajectory(3, 1.0, &[]);
1373 let (header, frames) = SimpleDcdReader::from_bytes(&bytes).unwrap();
1374 assert_eq!(header.n_frames, 0);
1375 assert!(frames.is_empty());
1376 }
1377
1378 #[test]
1379 fn append_dcd_frame_basic() {
1380 let mut writer = SimpleDcdWriter::new(2, 1.0);
1381 writer.add_frame(&[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]);
1382 let original = writer.to_bytes();
1383
1384 let new_frame: Vec<[f32; 3]> = vec![[2.0, 2.0, 2.0], [3.0, 3.0, 3.0]];
1385 let updated = append_dcd_frame(&original, &new_frame).unwrap();
1386
1387 let (header, frames) = SimpleDcdReader::from_bytes(&updated).unwrap();
1388 assert_eq!(header.n_frames, 2);
1389 assert_eq!(frames.len(), 2);
1390 assert!(approx_eq(frames[1].x[0], 2.0));
1391 }
1392
1393 #[test]
1396 fn apply_xtc_precision_basic() {
1397 let mut writer = SimpleXtcWriter::new();
1398 writer.add_frame(0, 0.0, vec![[1.23456, 7.89012, -3.15625]]);
1399 apply_xtc_precision(&mut writer, 2);
1400 let p = &writer.frames[0].positions[0];
1401 assert!(approx_eq(p[0], 1.23) || (p[0] - 1.23).abs() < 0.005);
1402 }
1403
1404 #[test]
1405 fn xtc_recompress_roundtrip() {
1406 let mut writer = SimpleXtcWriter::new();
1407 writer.add_frame(0, 0.0, vec![[1.234, 5.678, 9.012]]);
1408 writer.add_frame(1, 1.0, vec![[0.111, 0.222, 0.333]]);
1409 let original_bytes = writer.to_bytes();
1410 let recompressed = xtc_recompress(&original_bytes, 3).unwrap();
1411 let frames = SimpleXtcReader::from_bytes(&recompressed).unwrap();
1412 assert_eq!(frames.len(), 2);
1413 assert!((frames[0].positions[0][0] - 1.234).abs() < 0.001);
1415 }
1416
1417 #[test]
1418 fn compute_rmsd_identical() {
1419 let positions = vec![[1.0f32, 0.0, 0.0], [0.0, 1.0, 0.0]];
1420 let rmsd = compute_rmsd(&positions, &positions);
1421 assert!(
1422 rmsd.abs() < 1e-6,
1423 "RMSD of identical sets should be 0, got {rmsd}"
1424 );
1425 }
1426
1427 #[test]
1428 fn compute_rmsd_known() {
1429 let a = vec![[0.0f32, 0.0, 0.0]];
1430 let b = vec![[3.0f32, 4.0, 0.0]]; let rmsd = compute_rmsd(&a, &b);
1432 assert!(approx_eq(rmsd, 5.0), "RMSD should be 5.0, got {rmsd}");
1433 }
1434
1435 #[test]
1436 fn compute_rmsd_different_lengths() {
1437 let a = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
1438 let b = vec![[0.0f32, 0.0, 0.0]];
1439 let rmsd = compute_rmsd(&a, &b);
1440 assert!(approx_eq(rmsd, 0.0)); }
1442
1443 #[test]
1446 fn xtc_to_dcd_basic() {
1447 let mut writer = SimpleXtcWriter::new();
1448 writer.add_frame(0, 0.0, vec![[1.0, 2.0, 3.0]]);
1449 writer.add_frame(1, 1.0, vec![[4.0, 5.0, 6.0]]);
1450 let xtc_bytes = writer.to_bytes();
1451
1452 let dcd_bytes = xtc_to_dcd(&xtc_bytes).unwrap();
1453 let (header, frames) = SimpleDcdReader::from_bytes(&dcd_bytes).unwrap();
1454 assert_eq!(header.n_atoms, 1);
1455 assert_eq!(header.n_frames, 2);
1456 assert_eq!(frames.len(), 2);
1457 assert!(approx_eq(frames[0].x[0], 1.0));
1459 assert!(approx_eq(frames[1].y[0], 5.0));
1460 }
1461
1462 #[test]
1463 fn xtc_to_dcd_empty() {
1464 let writer = SimpleXtcWriter::new();
1465 let xtc_bytes = writer.to_bytes();
1466 let dcd_bytes = xtc_to_dcd(&xtc_bytes).unwrap();
1467 let (header, frames) = SimpleDcdReader::from_bytes(&dcd_bytes).unwrap();
1468 assert_eq!(header.n_frames, 0);
1469 assert!(frames.is_empty());
1470 }
1471
1472 #[test]
1473 fn dcd_to_xtc_basic() {
1474 let mut writer = SimpleDcdWriter::new(2, 1.0);
1475 writer.add_frame(&[[10.0, 20.0, 30.0], [40.0, 50.0, 60.0]]);
1476 writer.add_frame(&[[11.0, 21.0, 31.0], [41.0, 51.0, 61.0]]);
1477 let dcd_bytes = writer.to_bytes();
1478
1479 let xtc_bytes = dcd_to_xtc(&dcd_bytes).unwrap();
1480 let frames = SimpleXtcReader::from_bytes(&xtc_bytes).unwrap();
1481 assert_eq!(frames.len(), 2);
1482 assert!(approx_eq(frames[0].positions[0][0], 1.0));
1484 assert!(approx_eq(frames[0].positions[1][2], 6.0));
1485 assert!(approx_eq(frames[1].positions[0][0], 1.1));
1486 }
1487
1488 #[test]
1489 fn xtc_dcd_xtc_roundtrip() {
1490 let positions_nm = vec![[10.0f32, 20.0, 30.0], [40.0, 50.0, 60.0]];
1495 let mut xtc_writer = SimpleXtcWriter::new();
1496 xtc_writer.add_frame(0, 0.0, positions_nm.clone());
1497 let xtc_bytes = xtc_writer.to_bytes();
1498
1499 let dcd_bytes = xtc_to_dcd(&xtc_bytes).unwrap();
1500 let xtc2_bytes = dcd_to_xtc(&dcd_bytes).unwrap();
1501 let frames = SimpleXtcReader::from_bytes(&xtc2_bytes).unwrap();
1502 assert_eq!(frames.len(), 1);
1503 assert!(
1505 approx_eq(frames[0].positions[0][0], 1.0),
1506 "expected 1.0, got {}",
1507 frames[0].positions[0][0]
1508 );
1509 assert!(
1510 approx_eq(frames[0].positions[1][0], 4.0),
1511 "expected 4.0, got {}",
1512 frames[0].positions[1][0]
1513 );
1514 }
1515
1516 #[test]
1519 fn xtc_extract_frame_times_basic() {
1520 let mut writer = SimpleXtcWriter::new();
1521 writer.add_frame(0, 0.0, vec![[0.0; 3]]);
1522 writer.add_frame(1, 2.5, vec![[0.0; 3]]);
1523 writer.add_frame(2, 5.0, vec![[0.0; 3]]);
1524 let bytes = writer.to_bytes();
1525 let times = xtc_extract_frame_times(&bytes).unwrap();
1526 assert_eq!(times.len(), 3);
1527 assert!(approx_eq(times[0], 0.0));
1528 assert!(approx_eq(times[1], 2.5));
1529 assert!(approx_eq(times[2], 5.0));
1530 }
1531
1532 #[test]
1533 fn xtc_extract_frame_steps_basic() {
1534 let mut writer = SimpleXtcWriter::new();
1535 writer.add_frame(10, 0.0, vec![[0.0; 3]]);
1536 writer.add_frame(20, 1.0, vec![[0.0; 3]]);
1537 let bytes = writer.to_bytes();
1538 let steps = xtc_extract_frame_steps(&bytes).unwrap();
1539 assert_eq!(steps, vec![10, 20]);
1540 }
1541
1542 #[test]
1543 fn xtc_frame_time_deltas_basic() {
1544 let mut writer = SimpleXtcWriter::new();
1545 writer.add_frame(0, 0.0, vec![[0.0; 3]]);
1546 writer.add_frame(1, 1.0, vec![[0.0; 3]]);
1547 writer.add_frame(2, 3.0, vec![[0.0; 3]]);
1548 let bytes = writer.to_bytes();
1549 let deltas = xtc_frame_time_deltas(&bytes).unwrap();
1550 assert_eq!(deltas.len(), 2);
1551 assert!(approx_eq(deltas[0], 1.0));
1552 assert!(approx_eq(deltas[1], 2.0));
1553 }
1554
1555 #[test]
1556 fn xtc_frame_time_deltas_single_frame() {
1557 let mut writer = SimpleXtcWriter::new();
1558 writer.add_frame(0, 0.0, vec![[0.0; 3]]);
1559 let bytes = writer.to_bytes();
1560 let deltas = xtc_frame_time_deltas(&bytes).unwrap();
1561 assert!(deltas.is_empty());
1562 }
1563
1564 #[test]
1567 fn xtc_frame_with_velocity_kinetic_energy() {
1568 let frame = XtcFrame {
1569 step: 0,
1570 time: 0.0,
1571 box_matrix: [[1.0; 3]; 3],
1572 positions: vec![[0.0; 3]],
1573 };
1574 let velocities = vec![[3.0f32, 4.0, 0.0]]; let wv = XtcFrameWithVelocity::new(frame, velocities);
1576 assert!((wv.kinetic_energy_proxy() - 12.5).abs() < 1e-5);
1577 }
1578
1579 #[test]
1580 fn xtc_frame_with_velocity_rms_speed() {
1581 let frame = XtcFrame {
1582 step: 0,
1583 time: 0.0,
1584 box_matrix: [[1.0; 3]; 3],
1585 positions: vec![[0.0; 3]; 4],
1586 };
1587 let velocities = vec![[2.0f32, 0.0, 0.0]; 4];
1589 let wv = XtcFrameWithVelocity::new(frame, velocities);
1590 assert!(approx_eq(wv.rms_speed(), 2.0));
1591 }
1592
1593 #[test]
1594 fn xtc_frame_with_velocity_max_speed() {
1595 let frame = XtcFrame {
1596 step: 0,
1597 time: 0.0,
1598 box_matrix: [[1.0; 3]; 3],
1599 positions: vec![[0.0; 3]; 3],
1600 };
1601 let velocities = vec![[1.0f32, 0.0, 0.0], [3.0, 4.0, 0.0], [0.1, 0.0, 0.0]];
1602 let wv = XtcFrameWithVelocity::new(frame, velocities);
1603 assert!(approx_eq(wv.max_speed(), 5.0)); }
1605
1606 #[test]
1607 fn xtc_velocity_writer_basic() {
1608 let mut writer = XtcVelocityWriter::new();
1609 let pos = vec![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
1610 let vel = vec![[0.1f32, 0.0, 0.0], [0.0, 0.2, 0.0]];
1611 writer.add_frame(0, 0.0, pos, vel);
1612 assert_eq!(writer.frame_count(), 1);
1613 let bytes = writer.to_bytes();
1614 assert!(!bytes.is_empty());
1615 let magic_pos = bytes.windows(4).position(|w| w == b"OXVL");
1617 assert!(magic_pos.is_some(), "should contain OXVL magic");
1618 }
1619
1620 #[test]
1621 fn xtc_velocity_writer_kinetic_energy() {
1622 let mut writer = XtcVelocityWriter::new();
1623 let pos = vec![[0.0f32; 3]];
1624 let vel = vec![[3.0f32, 4.0, 0.0]]; writer.add_frame(0, 0.0, pos, vel);
1626 let ke = writer.frame_kinetic_energy(0);
1627 assert!((ke - 12.5).abs() < 1e-5);
1628 assert!(approx_eq(writer.frame_kinetic_energy(99), 0.0));
1630 }
1631
1632 #[test]
1633 fn compute_finite_difference_velocities_basic() {
1634 let frames = vec![
1635 XtcFrame {
1636 step: 0,
1637 time: 0.0,
1638 box_matrix: [[1.0; 3]; 3],
1639 positions: vec![[0.0, 0.0, 0.0]],
1640 },
1641 XtcFrame {
1642 step: 1,
1643 time: 1.0,
1644 box_matrix: [[1.0; 3]; 3],
1645 positions: vec![[2.0, 0.0, 0.0]],
1646 },
1647 ];
1648 let vels = compute_finite_difference_velocities(&frames, 1.0);
1649 assert_eq!(vels.len(), 1);
1650 assert!(approx_eq(vels[0][0][0], 2.0)); }
1652
1653 #[test]
1654 fn compute_finite_difference_velocities_three_frames() {
1655 let frames: Vec<XtcFrame> = (0..3)
1656 .map(|i| XtcFrame {
1657 step: i,
1658 time: i as f32,
1659 box_matrix: [[1.0; 3]; 3],
1660 positions: vec![[i as f32 * 3.0, 0.0, 0.0]],
1661 })
1662 .collect();
1663 let vels = compute_finite_difference_velocities(&frames, 1.0);
1664 assert_eq!(vels.len(), 2);
1666 assert!(approx_eq(vels[0][0][0], 3.0));
1668 assert!(approx_eq(vels[1][0][0], 3.0));
1669 }
1670
1671 #[test]
1672 fn compute_finite_difference_velocities_single_frame() {
1673 let frames = vec![XtcFrame {
1674 step: 0,
1675 time: 0.0,
1676 box_matrix: [[1.0; 3]; 3],
1677 positions: vec![[0.0; 3]],
1678 }];
1679 let vels = compute_finite_difference_velocities(&frames, 1.0);
1680 assert!(vels.is_empty());
1681 }
1682}