molly/lib.rs
1use std::fs::File;
2use std::io::{self, Read, Seek, SeekFrom, Write};
3use std::{cell::Cell, path::Path};
4
5use reader::read_nbytes;
6
7use crate::buffer::{Buffer, UnBuffered};
8use crate::reader::{
9 read_boxvec, read_compressed_positions, read_f32, read_f32s, read_i32, read_u32,
10};
11use crate::selection::{AtomSelection, FrameSelection};
12use crate::writer::{write_compressed_positions, write_int_positions};
13
14pub mod buffer;
15pub mod reader;
16pub mod selection;
17pub mod writer;
18
19// See https://gitlab.com/gromacs/gromacs/-/blob/v2024.1/src/gromacs/fileio/xdrf.h?ref_type=tags#L78
20pub const XTC_1995_MAX_NATOMS: usize = 298261617;
21
22thread_local! {
23 /// A scratch buffer to read encoded bytes into for subsequent decoding.
24 static SCRATCH: Cell<Vec<u8>> = const { Cell::new(Vec::new()) };
25}
26
27/// Box vectors.
28///
29/// Stored as a column-major matrix of Gromacs box vectors.
30pub type BoxVec = [f32; 9];
31
32/// Three-dimensional position in nanometers.
33pub type Position = [f32; 3];
34
35#[repr(i32)]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Magic {
38 Xtc1995 = 1995,
39 Xtc2023 = 2023,
40}
41
42impl Magic {
43 pub const XTC_1995: i32 = Magic::Xtc1995 as _;
44 pub const XTC_2023: i32 = Magic::Xtc2023 as _;
45
46 fn to_be_bytes(&self) -> [u8; 4] {
47 (*self as i32).to_be_bytes()
48 }
49}
50
51impl TryFrom<i32> for Magic {
52 type Error = String;
53
54 fn try_from(value: i32) -> Result<Self, Self::Error> {
55 match value {
56 Magic::XTC_1995 => Ok(Self::Xtc1995),
57 Magic::XTC_2023 => Ok(Self::Xtc2023),
58 unknown => Err(format!(
59 "found invalid magic number '{unknown}' ({unknown:#0x}), {} and {} are supported",
60 Self::XTC_1995,
61 Self::XTC_2023
62 )),
63 }
64 }
65}
66
67impl std::fmt::Display for Magic {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}", *self as i32)
70 }
71}
72
73/// The header of a single xtc frame.
74pub struct Header {
75 pub magic: Magic,
76 pub natoms: usize,
77 pub step: u32,
78 pub time: f32,
79 pub boxvec: BoxVec,
80 pub natoms_repeated: usize,
81}
82
83impl Header {
84 pub const SIZE: usize = 4 * (5 + 9);
85
86 pub fn read(file: &mut impl Read) -> io::Result<Self> {
87 let magic = Magic::try_from(read_i32(file)?)
88 .map_err(|err| io::Error::other(format!("could not read header: {err}")))?;
89 let natoms: usize = read_u32(file)?
90 .try_into()
91 .map_err(|err| io::Error::other(format!("could not read natoms: {err}")))?;
92 let step: u32 = read_u32(file)?;
93 let time = read_f32(file)?;
94
95 // Read the frame data.
96 let boxvec = read_boxvec(file)?;
97 let natoms_repeated = read_u32(file)?
98 .try_into()
99 .map_err(|err| io::Error::other(format!("could not read second natoms: {err}")))?;
100 assert_eq!(natoms, natoms_repeated);
101
102 Ok(Header {
103 magic,
104 natoms,
105 step,
106 time,
107 boxvec,
108 natoms_repeated,
109 })
110 }
111
112 pub fn to_be_bytes(&self) -> [u8; Self::SIZE] {
113 let mut bytes = Vec::new();
114 bytes.extend(self.magic.to_be_bytes()); // i32
115
116 let natoms = u32::try_from(self.natoms).unwrap().to_be_bytes();
117 bytes.extend(natoms); // u32
118 bytes.extend(self.step.to_be_bytes()); // u32
119 bytes.extend(self.time.to_be_bytes()); // f32
120 bytes.extend(self.boxvec.map(f32::to_be_bytes).iter().flatten()); // 9 × f32
121 assert_eq!(self.natoms, self.natoms_repeated);
122 bytes.extend(natoms); // u32
123
124 bytes.try_into().unwrap()
125 }
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub struct Frame {
130 pub step: u32,
131 /// Time in picoseconds.
132 pub time: f32,
133 /// Box vectors.
134 ///
135 /// Stored as a column-major matrix of Gromacs box vectors.
136 pub boxvec: BoxVec,
137 pub precision: f32,
138 pub positions: Vec<f32>,
139}
140
141impl Frame {
142 /// Returns a 2D column-major representation of the `boxvec` of this [`Frame`].
143 pub fn boxvec_cols_2d(&self) -> [[f32; 3]; 3] {
144 // Though a solution based on, say, .as_chunks() would be much nicer, it is not available
145 // in the minimum supported rust version.
146 let mut res = [[0.0; 3]; 3];
147 for (src, dst) in self.boxvec.chunks_exact(3).zip(&mut res) {
148 dst.copy_from_slice(src);
149 }
150 res
151 }
152
153 /// Returns an iterator over the coordinates stored in this [`Frame`].
154 pub fn coords(&self) -> impl Iterator<Item = Position> + '_ {
155 // We can safely unwrap here, because we know the chunks are exactly three wide.
156 self.positions
157 .chunks_exact(3)
158 .map(|p| p.try_into().unwrap())
159 }
160
161 /// Returns the number of atoms in this [`Frame`].
162 pub fn natoms(&self) -> usize {
163 let npos = self.positions.len();
164 assert_eq!(
165 npos % 3,
166 0,
167 "the number of single positions in a frame must always be a multiple of 3"
168 );
169 npos / 3
170 }
171}
172
173impl Default for Frame {
174 fn default() -> Self {
175 Frame {
176 step: Default::default(),
177 time: Default::default(),
178 boxvec: Default::default(),
179 precision: 1000.,
180 positions: Default::default(),
181 }
182 }
183}
184
185/// Calculate the xdr padding for some number of bytes.
186#[doc(hidden)]
187pub fn padding(n: usize) -> usize {
188 (4 - (n % 4)) % 4
189}
190
191/// Read the positions in a frame after the header.
192///
193/// If successful, returns the number of compressed bytes that were read.
194///
195/// Internal use.
196#[doc(hidden)]
197pub fn read_positions<'s, 'r, B: buffer::Buffered<'s, 'r, R>, R: Read>(
198 file: &'r mut R,
199 header_natoms: usize,
200 scratch: &'s mut Vec<u8>,
201 frame: &mut Frame,
202 atom_selection: &AtomSelection,
203 magic: Magic,
204) -> io::Result<usize> {
205 // If the atom_selection specifies fewer atoms, we will only allocate up to that point.
206 let natoms_selected = atom_selection.natoms_selected(header_natoms);
207
208 // Resize the positions array for the selected number of atoms.
209 frame.positions.resize(natoms_selected * 3, f32::NAN);
210 frame.precision = read_f32(file)?;
211 read_compressed_positions::<B, R>(
212 file,
213 header_natoms,
214 &mut frame.positions,
215 frame.precision,
216 scratch,
217 atom_selection,
218 magic,
219 )
220}
221
222#[derive(Debug, Clone)]
223pub struct XTCReader<R> {
224 pub file: R,
225 pub step: usize,
226}
227
228impl XTCReader<std::fs::File> {
229 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
230 let file = std::fs::File::open(path)?;
231 Ok(Self::new(file))
232 }
233}
234
235impl<R: Read> XTCReader<R> {
236 pub fn new(reader: R) -> Self {
237 Self {
238 file: reader,
239 step: 0,
240 }
241 }
242
243 /// Read the header at the start of a frame.
244 ///
245 /// Assumes the internal reader is at the start of a new frame header.
246 pub fn read_header(&mut self) -> io::Result<Header> {
247 Header::read(&mut self.file)
248 }
249
250 /// Read a small number of uncompressed positions.
251 ///
252 /// If successful, returns the number of compressed bytes that were read.
253 ///
254 /// # Panics
255 ///
256 /// `natoms` must be 9 or less, otherwise the positions must be decompressed and cannot be read
257 /// directly through this function.
258 ///
259 /// Oh xtc, you are so fucking weird.
260 pub fn read_smol_positions(
261 &mut self,
262 natoms: usize,
263 frame: &mut Frame,
264 atom_selection: &AtomSelection,
265 ) -> io::Result<usize> {
266 assert!(
267 natoms <= 9,
268 "only read uncomprossed positions when the number of atoms is 9 or less"
269 );
270
271 // In case the number of atoms is very small, just read their uncompressed positions.
272 frame.positions.resize(natoms * 3, 0.0);
273 let mut buf = [0.0; 9 * 3]; // We have at most 9 atoms, so we handle them on the stack.
274 let buf = &mut buf[..natoms * 3];
275 read_f32s(&mut self.file, buf)?;
276 frame.positions.clear();
277 frame.positions.extend(
278 buf.chunks_exact(3)
279 .enumerate()
280 .filter_map(|(idx, pos): (usize, &[f32])| -> Option<[f32; 3]> {
281 if atom_selection.is_included(idx).unwrap_or_default() {
282 Some(pos.try_into().unwrap())
283 } else {
284 None
285 }
286 })
287 .flatten(),
288 );
289 // TODO: It is unclear to me what to do with the precision in this case. It is
290 // basically invalid, or just irrelevant here, since we don't decode them. They were
291 // never compressed to begin with.
292
293 Ok(std::mem::size_of_val(buf))
294 }
295
296 /// A convenience function to read all frames in a trajectory.
297 ///
298 /// It is likely more efficient to use [`XTCReader::read_frame`] if you are only interested in
299 /// the values of a single frame at a time.
300 pub fn read_all_frames(&mut self) -> io::Result<Box<[Frame]>> {
301 let mut frames = Vec::new();
302 loop {
303 let mut frame = Frame::default();
304 if let Err(err) = self.read_frame(&mut frame) {
305 match err.kind() {
306 // We have found the end of the file. No more frames, we're done.
307 io::ErrorKind::UnexpectedEof => break,
308 // Something else went wrong...
309 _ => Err(err)?,
310 }
311 }
312 frames.push(frame);
313 }
314 Ok(frames.into_boxed_slice())
315 }
316
317 /// Reads and returns a [`Frame`] and advances one step.
318 pub fn read_frame(&mut self, frame: &mut Frame) -> io::Result<()> {
319 self.read_frame_with_selection(frame, &AtomSelection::All)
320 }
321
322 /// Reads and returns a [`Frame`] according to the [`AtomSelection`], and advances one step.
323 pub fn read_frame_with_selection(
324 &mut self,
325 frame: &mut Frame,
326 atom_selection: &AtomSelection,
327 ) -> io::Result<()> {
328 // Take the thread-local SCRATCH and use that while decoding the values.
329 let mut scratch = SCRATCH.take();
330 self.read_frame_with_scratch(frame, &mut scratch, atom_selection)
331 }
332
333 /// Reads and returns a [`Frame`] and advances one step, internally reading the compressed data
334 /// into `scratch`.
335 ///
336 /// # Note
337 ///
338 /// This function performs the work of [`XTCReader::read_frame`], but leaves all allocations to
339 /// the caller.
340 ///
341 /// The contents of `scratch` should not be depended upon! It just serves as a scratch buffer
342 /// for the inner workings of decoding.
343 ///
344 /// In most cases, [`XTCReader::read_frame`] is more than sufficient. This function only serves
345 /// to make specific optimization possible.
346 pub fn read_frame_with_scratch(
347 &mut self,
348 frame: &mut Frame,
349 scratch: &mut Vec<u8>,
350 atom_selection: &AtomSelection,
351 ) -> io::Result<()> {
352 self.read_frame_with_scratch_impl::<UnBuffered>(frame, scratch, atom_selection)
353 }
354
355 /// Implementation of reading a frame with a scratch buffer.
356 fn read_frame_with_scratch_impl<'s, 'r, B: buffer::Buffered<'s, 'r, R>>(
357 &'r mut self,
358 frame: &mut Frame,
359 scratch: &'s mut Vec<u8>,
360 atom_selection: &AtomSelection,
361 ) -> io::Result<()> {
362 // Start of by reading the header.
363 let header = self.read_header()?;
364
365 // Now, we read the atoms.
366 if header.natoms <= 9 {
367 self.read_smol_positions(header.natoms, frame, atom_selection)?;
368 } else {
369 read_positions::<B, R>(
370 &mut self.file,
371 header.natoms,
372 scratch,
373 frame,
374 atom_selection,
375 header.magic,
376 )?;
377 }
378
379 self.step += 1;
380
381 frame.step = header.step;
382 frame.time = header.time;
383 frame.boxvec = header.boxvec;
384
385 Ok(())
386 }
387}
388
389impl XTCReader<File> {
390 /// Reset the reader to its initial position.
391 ///
392 /// Go back to the first frame.
393 pub fn home(&mut self) -> io::Result<()> {
394 self.file.seek(SeekFrom::Start(0))?;
395 self.step = 0;
396 Ok(())
397 }
398
399 /// Returns the offsets from the headers in this [`XTCReader<R>`] from its current position.
400 ///
401 /// The last value points one byte after the last byte in the reader.
402 ///
403 /// If this function is called when the internal reader is not at its starting position, the
404 /// frame offsets _from_ its position are determined. If you wish to determine the offsets from
405 /// the initial reader position, call [`XTCReader::home`] before calling this function.
406 ///
407 /// # Errors
408 ///
409 /// This function will pass through any reader errors.
410 pub fn determine_offsets_exclusive(&mut self, until: Option<usize>) -> io::Result<Box<[u64]>> {
411 // Remember where we start so we can return to it later.
412 let start_pos = self.file.stream_position()?;
413
414 let mut offsets = Vec::new();
415
416 while until.map_or(true, |until| offsets.len() < until) {
417 let header = match self.read_header() {
418 Ok(header) => header,
419 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
420 Err(err) => Err(err)?,
421 };
422
423 let offset = self.skip_positions(&header)?;
424 offsets.push(offset);
425 }
426
427 // Return back to where we started.
428 self.file.seek(SeekFrom::Start(start_pos))?;
429
430 Ok(offsets.into_boxed_slice())
431 }
432
433 /// Returns the offsets of this [`XTCReader<R>`] from its current position.
434 ///
435 /// The last value points to the start of the last frame.
436 ///
437 /// If this function is called when the internal reader is not at its starting position, the
438 /// frame offsets _from_ its position are determined. If you wish to determine the offsets from
439 /// the initial reader position, call [`XTCReader::home`] before calling this function.
440 ///
441 /// # Errors
442 ///
443 /// This function will pass through any reader errors.
444 pub fn determine_offsets(&mut self, until: Option<usize>) -> io::Result<Box<[u64]>> {
445 let mut offsets = vec![0];
446 let exclusive = self.determine_offsets_exclusive(until)?;
447 offsets.extend(exclusive.iter().take(exclusive.len().saturating_sub(1)));
448 Ok(offsets.into_boxed_slice())
449 }
450
451 /// Returns the frame sizes of this [`XTCReader<R>`].
452 ///
453 /// # Errors
454 ///
455 /// This function will pass through any reader errors.
456 pub fn determine_frame_sizes(&mut self, until: Option<usize>) -> io::Result<Box<[u64]>> {
457 let starts = self.determine_offsets_exclusive(until)?;
458 let ends = starts.iter().clone().skip(1);
459 Ok(starts
460 .iter()
461 .zip(ends)
462 .map(|(s, e)| e - s)
463 .collect::<Vec<_>>()
464 .into_boxed_slice())
465 }
466
467 /// Seeks to offset, then reads and returns a [`Frame`] and advances one step.
468 ///
469 /// # Note
470 ///
471 /// The `BUFFERED` const generic value can be used to set whether the frame reader will read in
472 /// a buffered manner or not at compile time.
473 ///
474 /// Buffered reading is most favorable when a small number of positions are read from the top of
475 /// the frame (leaving many positions that do not need to be read at the bottom), especially at the
476 /// point where disk read speed is a bottleneck.
477 pub fn read_frame_at_offset<const BUFFERED: bool>(
478 &mut self,
479 frame: &mut Frame,
480 offset: u64,
481 atom_selection: &AtomSelection,
482 ) -> io::Result<()> {
483 self.file.seek(SeekFrom::Start(offset))?;
484 match BUFFERED {
485 false => self.read_frame_with_selection(frame, atom_selection),
486 true => self.read_frame_with_selection_buffered(frame, atom_selection),
487 }
488 }
489
490 /// Append [`Frame`]s to the `frames` buffer according to a [`Selection`].
491 ///
492 /// If successful, it will return the number of frames that were read.
493 /// This can be useful since the selection itself is not enough to tell how many frames will
494 /// actually be read.
495 ///
496 /// # Note
497 ///
498 /// The `BUFFERED` const generic value can be used to set whether the frame reader will read in
499 /// a buffered manner or not at compile time.
500 ///
501 /// Buffered reading is most favorable when a small number of positions are read from the top of
502 /// the frame (leaving many positions that do not need to be read at the bottom), especially at the
503 /// point where disk read speed is a bottleneck.
504 pub fn read_frames<const BUFFERED: bool>(
505 &mut self,
506 frames: &mut impl Extend<Frame>,
507 frame_selection: &FrameSelection,
508 atom_selection: &AtomSelection,
509 ) -> io::Result<usize> {
510 let offsets = self.determine_offsets(frame_selection.until())?;
511 let mut n = 0;
512 for (idx, &offset) in offsets.iter().enumerate() {
513 match frame_selection.is_included(idx) {
514 Some(true) => {}
515 Some(false) => continue,
516 None => break,
517 }
518 let mut frame = Frame::default();
519 self.read_frame_at_offset::<BUFFERED>(&mut frame, offset, atom_selection)?;
520 frames.extend(Some(frame));
521 n += 1;
522 }
523
524 Ok(n)
525 }
526
527 /// Reads and returns a [`Frame`] according to the [`AtomSelection`], and advances one step.
528 pub fn read_frame_with_selection_buffered(
529 &mut self,
530 frame: &mut Frame,
531 atom_selection: &AtomSelection,
532 ) -> io::Result<()> {
533 // Take the thread-local SCRATCH and use that while decoding the values.
534 let mut scratch = SCRATCH.take();
535 self.read_frame_with_scratch_buffered(frame, &mut scratch, atom_selection)
536 }
537
538 /// Reads and returns a [`Frame`] and advances one step, internally reading the compressed data
539 /// into `scratch`.
540 ///
541 /// # Note
542 ///
543 /// This function performs the work of [`XTCReader::read_frame`], but leaves all allocations to
544 /// the caller.
545 ///
546 /// The contents of `scratch` should not be depended upon! It just serves as a scratch buffer
547 /// for the inner workings of decoding.
548 ///
549 /// In most cases, [`XTCReader::read_frame`] is more than sufficient. This function only serves
550 /// to make specific optimization possible.
551 pub fn read_frame_with_scratch_buffered(
552 &mut self,
553 frame: &mut Frame,
554 scratch: &mut Vec<u8>,
555 atom_selection: &AtomSelection,
556 ) -> io::Result<()> {
557 self.read_frame_with_scratch_impl::<Buffer>(frame, scratch, atom_selection)
558 }
559
560 /// Skip positions section of the current frame and jump to the start of the next frame.
561 ///
562 /// Assumes the file position is right after the `header` that is passed.
563 /// Returns the file offset of the start of the new frame.
564 pub fn skip_positions(&mut self, header: &Header) -> io::Result<u64> {
565 let skip = if header.natoms <= 9 {
566 // Know how many bytes are in this frame until the next header since the positions are
567 // uncompressed.
568 header.natoms as u64 * 3 * 4
569 } else {
570 // We need to read the nbytes value to get the offset until the next header.
571 self.file.seek(SeekFrom::Current(32))?;
572 // The size of the buffer is stored either as a 64 or 32-bit integer, depending on the
573 // magic number in the header.
574 let nbytes = read_nbytes(&mut self.file, header.magic)? as u64;
575 nbytes + padding(nbytes as usize) as u64
576 };
577 let offset = self.file.seek(SeekFrom::Current(skip as i64))?;
578 Ok(offset)
579 }
580
581 /// Skip frames until a frame where `time >= t` is found.
582 ///
583 /// If such frame is found, its header is returned and the file position is set to the start of
584 /// the frame.
585 ///
586 /// Assumes the file position is at the start of a frame.
587 pub fn skip_to_time(&mut self, t: f32) -> io::Result<Header> {
588 loop {
589 let pos = self.file.stream_position()?;
590 let header = self.read_header()?;
591 if header.time >= t {
592 // Go back to the start of this frame and return its header.
593 self.file.seek(SeekFrom::Start(pos))?;
594 return Ok(header);
595 }
596 // Skip to the next frame.
597 self.skip_positions(&header)?;
598 }
599 }
600
601 /// Skip forward `n` frames.
602 ///
603 /// Assumes the file position is at the start of the frame.
604 pub fn skip_frames(&mut self, n: u64) -> io::Result<()> {
605 for _ in 0..n {
606 self.seek_next()?
607 }
608 Ok(())
609 }
610
611 /// Step to the next frame.
612 ///
613 /// Assumes the file position is at the start of the frame.
614 pub fn seek_next(&mut self) -> io::Result<()> {
615 let header = self.read_header()?;
616 self.skip_positions(&header)?;
617 Ok(())
618 }
619
620 /// Step back one frame.
621 ///
622 /// If successful, the header of the previous frame is returned and the file position is set to
623 /// the start of that frame.
624 ///
625 /// Assumes the file position is at the start of the frame.
626 ///
627 /// # Errors
628 ///
629 /// If the start of the file is crossed because no previous frame is encountered, this function
630 /// will return an error.
631 pub fn seek_prev(&mut self) -> io::Result<Header> {
632 // An XTC file follows the XDR convention of consisting entirely out of 32-bit blocks.
633 const XDR_INT_SIZE: i64 = 4;
634 // Step back a little, because we don't want to read the header we are currently at.
635 // We know we can go back at least the number of bytes in a header, plus the precision
636 // field, plus the size of the `nbytes` field, plus the minimum number of compressed bytes.
637 //
638 // Depending on whether we're reading XTC 1995 or 2023, that `nbytes` field is a 32-bit or
639 // 64-bit integer, respectively. So we can say it is at least one XDR_INT_SIZE. The
640 // precision field is worth one XDR_INT_SIZE as well.
641 // The minimum number of bytes for the compressed positions section could certainly be
642 // computed, but it would depend on the precision and that makes this simple optimization
643 // trickier than necessary.
644 self.file
645 .seek(SeekFrom::Current(-(Header::SIZE as i64 + XDR_INT_SIZE * 2)))?;
646 // Try reading a header until we find one, or we reach the start of the file.
647 loop {
648 // Remember the current offset to return to it if header will be read successfully.
649 let pos = self.file.stream_position()?;
650 if let Ok(header) = self.read_header() {
651 self.file.seek(SeekFrom::Start(pos))?;
652 return Ok(header);
653 } else {
654 // Header not found, keep seeking backwards. We go back by two integers' worth of
655 // bytes, because by trying to read the header and failing because the magic number
656 // was incorrect, we did end up reading one byte. Will fail if passing beyond the
657 // start of the file.
658 // NOTE: It is _technically_ possible (but realistically impossible) that while
659 // trying to read the `Header`, more than one integer is read.
660 self.file.seek(SeekFrom::Current(-2 * XDR_INT_SIZE))?;
661 }
662 }
663 }
664
665 /// Read the last frame in the trajectory.
666 pub fn read_last_frame(&mut self, frame: &mut Frame) -> io::Result<()> {
667 self.file.seek(SeekFrom::End(0))?;
668 self.seek_prev()?;
669 self.read_frame(frame)
670 }
671}
672
673/// Writer for XTC trajectory files.
674#[derive(Debug, Clone)]
675pub struct XTCWriter<W> {
676 pub file: W,
677 pub magic: Magic,
678}
679
680impl XTCWriter<std::fs::File> {
681 /// Create a new XTC file at the given path.
682 pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
683 let file = std::fs::File::create(path)?;
684 Ok(Self::new(file))
685 }
686}
687
688impl<W: Write> XTCWriter<W> {
689 /// Create a new XTC writer wrapping the given writer.
690 pub fn new(writer: W) -> Self {
691 Self::new_with_magic(writer, Magic::Xtc1995)
692 }
693
694 /// Create a new XTC writer with a specific magic number.
695 pub fn new_with_magic(writer: W, magic: Magic) -> Self {
696 Self {
697 file: writer,
698 magic,
699 }
700 }
701
702 /// Write a frame to the XTC file.
703 pub fn write_frame(&mut self, frame: &Frame) -> io::Result<()> {
704 let natoms = frame.natoms();
705
706 let header = Header {
707 magic: self.magic,
708 natoms,
709 step: frame.step,
710 time: frame.time,
711 boxvec: frame.boxvec,
712 natoms_repeated: natoms,
713 };
714
715 self.file.write_all(&header.to_be_bytes())?;
716
717 if natoms <= 9 {
718 self.write_smol_positions(&frame.positions)
719 } else {
720 self.file.write_all(&frame.precision.to_be_bytes())?;
721 write_compressed_positions(
722 &mut self.file,
723 &frame.positions,
724 frame.precision,
725 self.magic,
726 )?;
727 Ok(())
728 }
729 }
730
731 /// Write uncompressed positions for small frames (≤9 atoms).
732 fn write_smol_positions(&mut self, positions: &[f32]) -> io::Result<()> {
733 for &pos in positions {
734 self.file.write_all(&pos.to_be_bytes())?;
735 }
736 Ok(())
737 }
738
739 /// Write parts of the frame to the XTC file.
740 ///
741 /// Coordinates are sourced from any [`ExactSizeIterator`] yielding `&[f32; 3]`.
742 pub fn write_frame_parts<'a>(
743 &'a mut self,
744 step: u32,
745 time: f32,
746 boxvec: [f32; 9],
747 coords: impl ExactSizeIterator<Item = &'a [f32; 3]>,
748 precision: f32,
749 ) -> io::Result<()> {
750 let natoms = coords.len();
751 let header = Header {
752 magic: self.magic,
753 natoms,
754 step,
755 time,
756 boxvec,
757 natoms_repeated: natoms,
758 };
759
760 self.file.write_all(&header.to_be_bytes())?;
761
762 if natoms <= 9 {
763 let vec: Vec<f32> = coords.flatten().cloned().collect();
764 assert_eq!(vec.len(), natoms * 3);
765 self.write_smol_positions(&vec)
766 } else {
767 let to_int = |f: f32| (f * precision).round() as i32;
768 let mut int_coords: Vec<[i32; 3]> = coords
769 .map(|p| [to_int(p[0]), to_int(p[1]), to_int(p[2])])
770 .collect();
771 assert_eq!(int_coords.len(), natoms);
772
773 self.file.write_all(&precision.to_be_bytes())?;
774
775 write_int_positions(&mut self.file, &mut int_coords, self.magic)?;
776 Ok(())
777 }
778 }
779}