Skip to main content

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}