Skip to main content

readcon_core/
writer.rs

1use crate::types::ConFrame;
2use std::fs::File;
3use std::io::{self, BufWriter, Write};
4use std::path::Path;
5
6/// Default floating-point precision used for writing coordinates, cell dimensions, and masses.
7const DEFAULT_FLOAT_PRECISION: usize = 6;
8/// Always 0 or 1
9/// The value used to indicate a fixed atom in the output file.
10const FIXED_ATOM_FLAG: usize = 1;
11/// The value used to indicate a non-fixed (free) atom in the output file.
12const FREE_ATOM_FLAG: usize = 0;
13
14/// A writer that can serialize and write `ConFrame` objects to any output stream.
15///
16/// This struct encapsulates a writer (like a file) and provides a high-level API
17/// for writing simulation frames in the `.con` format.
18///
19/// # Example
20/// ```no_run
21/// # use std::fs::File;
22/// # use readcon_core::types::ConFrame;
23/// # use readcon_core::writer::ConFrameWriter;
24/// # let frames: Vec<ConFrame> = Vec::new();
25/// let mut writer = ConFrameWriter::from_path("output.con").unwrap();
26/// writer.extend(frames.iter()).unwrap();
27/// ```
28pub struct ConFrameWriter<W: Write> {
29    writer: BufWriter<W>,
30    precision: usize,
31}
32
33// General implementation for any type that implements `Write`.
34impl<W: Write> ConFrameWriter<W> {
35    /// Creates a new `ConFrameWriter` that wraps a given writer.
36    ///
37    /// # Arguments
38    ///
39    /// * `writer` - Any type that implements `std::io::Write`, e.g., a `File`.
40    pub fn new(writer: W) -> Self {
41        Self {
42            writer: BufWriter::new(writer),
43            precision: DEFAULT_FLOAT_PRECISION,
44        }
45    }
46
47    /// Creates a new `ConFrameWriter` with a custom floating-point precision.
48    ///
49    /// # Arguments
50    ///
51    /// * `writer` - Any type that implements `std::io::Write`.
52    /// * `precision` - Number of decimal places for floating-point output.
53    pub fn with_precision(writer: W, precision: usize) -> Self {
54        Self {
55            writer: BufWriter::new(writer),
56            precision,
57        }
58    }
59
60    /// Writes a single `ConFrame` to the output stream.
61    pub fn write_frame(&mut self, frame: &ConFrame) -> io::Result<()> {
62        let prec = self.precision;
63
64        // --- Write the 9-line Header ---
65        writeln!(self.writer, "{}", frame.header.prebox_header[0])?;
66        writeln!(self.writer, "{}", frame.header.prebox_header[1])?;
67        writeln!(
68            self.writer,
69            "{1:.0$} {2:.0$} {3:.0$}",
70            prec, frame.header.boxl[0], frame.header.boxl[1], frame.header.boxl[2]
71        )?;
72        writeln!(
73            self.writer,
74            "{1:.0$} {2:.0$} {3:.0$}",
75            prec, frame.header.angles[0], frame.header.angles[1], frame.header.angles[2]
76        )?;
77        writeln!(self.writer, "{}", frame.header.postbox_header[0])?;
78        writeln!(self.writer, "{}", frame.header.postbox_header[1])?;
79        writeln!(self.writer, "{}", frame.header.natm_types)?;
80
81        let natms_str: Vec<String> = frame
82            .header
83            .natms_per_type
84            .iter()
85            .map(|n| n.to_string())
86            .collect();
87        writeln!(self.writer, "{}", natms_str.join(" "))?;
88
89        let masses_str: Vec<String> = frame
90            .header
91            .masses_per_type
92            .iter()
93            .map(|m| format!("{:.1$}", m, prec))
94            .collect();
95        writeln!(self.writer, "{}", masses_str.join(" "))?;
96
97        // --- Write the Atom Data ---
98        let mut atom_idx_offset = 0;
99        for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
100            let symbol = &frame.atom_data[atom_idx_offset].symbol;
101            writeln!(self.writer, "{}", symbol)?;
102            writeln!(self.writer, "Coordinates of Component {}", type_idx + 1)?;
103
104            for i in 0..num_atoms_in_type {
105                let atom = &frame.atom_data[atom_idx_offset + i];
106                writeln!(
107                    self.writer,
108                    "{x:.prec$} {y:.prec$} {z:.prec$} {fixed_flag:.0} {atom_id}",
109                    prec = prec,
110                    x = atom.x,
111                    y = atom.y,
112                    z = atom.z,
113                    fixed_flag = if atom.is_fixed {
114                        FIXED_ATOM_FLAG
115                    } else {
116                        FREE_ATOM_FLAG
117                    },
118                    atom_id = atom.atom_id
119                )?;
120            }
121            atom_idx_offset += num_atoms_in_type;
122        }
123
124        // --- Write optional velocity section ---
125        if frame.has_velocities() {
126            // Blank separator line between coordinates and velocities
127            writeln!(self.writer)?;
128
129            let mut vel_idx_offset = 0;
130            for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
131                let symbol = &frame.atom_data[vel_idx_offset].symbol;
132                writeln!(self.writer, "{}", symbol)?;
133                writeln!(self.writer, "Velocities of Component {}", type_idx + 1)?;
134
135                for i in 0..num_atoms_in_type {
136                    let atom = &frame.atom_data[vel_idx_offset + i];
137                    writeln!(
138                        self.writer,
139                        "{vx:.prec$} {vy:.prec$} {vz:.prec$} {fixed_flag:.0} {atom_id}",
140                        prec = prec,
141                        vx = atom.vx.unwrap_or(0.0),
142                        vy = atom.vy.unwrap_or(0.0),
143                        vz = atom.vz.unwrap_or(0.0),
144                        fixed_flag = if atom.is_fixed {
145                            FIXED_ATOM_FLAG
146                        } else {
147                            FREE_ATOM_FLAG
148                        },
149                        atom_id = atom.atom_id
150                    )?;
151                }
152                vel_idx_offset += num_atoms_in_type;
153            }
154        }
155
156        Ok(())
157    }
158
159    /// Writes all frames from an iterator to the output stream.
160    ///
161    /// This is the most convenient way to write a multi-frame file.
162    pub fn extend<'a>(&mut self, frames: impl Iterator<Item = &'a ConFrame>) -> io::Result<()> {
163        for frame in frames {
164            self.write_frame(frame)?;
165        }
166        Ok(())
167    }
168}
169
170// Implementation block specifically for when the writer is a `File`.
171impl ConFrameWriter<File> {
172    /// Creates a new `ConFrameWriter` that writes to a file at the given path.
173    ///
174    /// This is a convenience function that creates the file and wraps it.
175    pub fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
176        let file = File::create(path)?;
177        Ok(Self::new(file))
178    }
179
180    /// Creates a new `ConFrameWriter` that writes to a file with a custom precision.
181    pub fn from_path_with_precision<P: AsRef<Path>>(path: P, precision: usize) -> io::Result<Self> {
182        let file = File::create(path)?;
183        Ok(Self::with_precision(file, precision))
184    }
185}