Skip to main content

readcon_core/
writer.rs

1use crate::types::{ConFrame, SECTION_FORCES, SECTION_VELOCITIES, encode_fixed_bitmask, meta};
2use serde_json::json;
3use std::fs::File;
4use std::io::{self, BufWriter, Write};
5use std::path::Path;
6
7/// Default floating-point precision used for writing coordinates, cell dimensions, and masses.
8const DEFAULT_FLOAT_PRECISION: usize = 6;
9
10/// A writer that can serialize and write `ConFrame` objects to any output stream.
11///
12/// This struct encapsulates a writer (like a file) and provides a high-level API
13/// for writing simulation frames in the `.con` format.
14///
15/// # Example
16/// ```no_run
17/// # use std::fs::File;
18/// # use readcon_core::types::ConFrame;
19/// # use readcon_core::writer::ConFrameWriter;
20/// # let frames: Vec<ConFrame> = Vec::new();
21/// let mut writer = ConFrameWriter::from_path("output.con").unwrap();
22/// writer.extend(frames.iter()).unwrap();
23/// ```
24pub struct ConFrameWriter<W: Write> {
25    writer: BufWriter<W>,
26    precision: usize,
27}
28
29// General implementation for any type that implements `Write`.
30impl<W: Write> ConFrameWriter<W> {
31    /// Creates a new `ConFrameWriter` that wraps a given writer.
32    ///
33    /// # Arguments
34    ///
35    /// * `writer` - Any type that implements `std::io::Write`, e.g., a `File`.
36    pub fn new(writer: W) -> Self {
37        Self {
38            writer: BufWriter::new(writer),
39            precision: DEFAULT_FLOAT_PRECISION,
40        }
41    }
42
43    /// Creates a new `ConFrameWriter` with a custom floating-point precision.
44    ///
45    /// # Arguments
46    ///
47    /// * `writer` - Any type that implements `std::io::Write`.
48    /// * `precision` - Number of decimal places for floating-point output.
49    pub fn with_precision(writer: W, precision: usize) -> Self {
50        Self {
51            writer: BufWriter::new(writer),
52            precision,
53        }
54    }
55
56    /// Writes a single `ConFrame` to the output stream.
57    pub fn write_frame(&mut self, frame: &ConFrame) -> io::Result<()> {
58        let prec = self.precision;
59
60        // --- Write the 9-line Header ---
61        writeln!(self.writer, "{}", frame.header.prebox_header.user)?;
62
63        // Line 2: always serialize JSON metadata with con_spec_version.
64        // Auto-populate sections based on frame data.
65        let mut meta_obj = serde_json::Map::new();
66        meta_obj.insert(
67            meta::CON_SPEC_VERSION.into(),
68            json!(frame.header.spec_version),
69        );
70        // Build sections array from actual data presence
71        let mut sections = Vec::new();
72        if frame.has_velocities() {
73            sections.push(json!(SECTION_VELOCITIES));
74        }
75        if frame.has_forces() {
76            sections.push(json!(SECTION_FORCES));
77        }
78        let validate = frame
79            .header
80            .metadata
81            .get(meta::VALIDATE)
82            .and_then(|value| value.as_bool())
83            .unwrap_or(false);
84        if !sections.is_empty() || validate {
85            meta_obj.insert(meta::SECTIONS.into(), json!(sections));
86        }
87        for (k, v) in &frame.header.metadata {
88            if k == meta::CON_SPEC_VERSION || k == meta::SECTIONS {
89                continue;
90            }
91            meta_obj.insert(k.clone(), v.clone());
92        }
93        writeln!(self.writer, "{}", serde_json::Value::Object(meta_obj))?;
94        writeln!(
95            self.writer,
96            "{1:.0$} {2:.0$} {3:.0$}",
97            prec, frame.header.boxl[0], frame.header.boxl[1], frame.header.boxl[2]
98        )?;
99        writeln!(
100            self.writer,
101            "{1:.0$} {2:.0$} {3:.0$}",
102            prec, frame.header.angles[0], frame.header.angles[1], frame.header.angles[2]
103        )?;
104        writeln!(self.writer, "{}", frame.header.postbox_header[0])?;
105        writeln!(self.writer, "{}", frame.header.postbox_header[1])?;
106        writeln!(self.writer, "{}", frame.header.natm_types)?;
107
108        let natms_str: Vec<String> = frame
109            .header
110            .natms_per_type
111            .iter()
112            .map(|n| n.to_string())
113            .collect();
114        writeln!(self.writer, "{}", natms_str.join(" "))?;
115
116        let masses_str: Vec<String> = frame
117            .header
118            .masses_per_type
119            .iter()
120            .map(|m| format!("{:.1$}", m, prec))
121            .collect();
122        writeln!(self.writer, "{}", masses_str.join(" "))?;
123
124        // --- Write the Atom Data ---
125        let mut atom_idx_offset = 0;
126        for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
127            let symbol = &frame.atom_data[atom_idx_offset].symbol;
128            writeln!(self.writer, "{}", symbol)?;
129            writeln!(self.writer, "Coordinates of Component {}", type_idx + 1)?;
130
131            for i in 0..num_atoms_in_type {
132                let atom = &frame.atom_data[atom_idx_offset + i];
133                writeln!(
134                    self.writer,
135                    "{x:.prec$} {y:.prec$} {z:.prec$} {fixed_flag} {atom_id}",
136                    prec = prec,
137                    x = atom.x,
138                    y = atom.y,
139                    z = atom.z,
140                    fixed_flag = encode_fixed_bitmask(atom.fixed),
141                    atom_id = atom.atom_id
142                )?;
143            }
144            atom_idx_offset += num_atoms_in_type;
145        }
146
147        // --- Write optional velocity section ---
148        if frame.has_velocities() {
149            // Blank separator line between coordinates and velocities
150            writeln!(self.writer)?;
151
152            let mut vel_idx_offset = 0;
153            for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
154                let symbol = &frame.atom_data[vel_idx_offset].symbol;
155                writeln!(self.writer, "{}", symbol)?;
156                writeln!(self.writer, "Velocities of Component {}", type_idx + 1)?;
157
158                for i in 0..num_atoms_in_type {
159                    let atom = &frame.atom_data[vel_idx_offset + i];
160                    let [vx, vy, vz] = atom.velocity.unwrap_or([0.0; 3]);
161                    writeln!(
162                        self.writer,
163                        "{vx:.prec$} {vy:.prec$} {vz:.prec$} {fixed_flag} {atom_id}",
164                        prec = prec,
165                        fixed_flag = encode_fixed_bitmask(atom.fixed),
166                        atom_id = atom.atom_id
167                    )?;
168                }
169                vel_idx_offset += num_atoms_in_type;
170            }
171        }
172
173        // --- Write optional force section ---
174        if frame.has_forces() {
175            // Blank separator line
176            writeln!(self.writer)?;
177
178            let mut force_idx_offset = 0;
179            for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
180                let symbol = &frame.atom_data[force_idx_offset].symbol;
181                writeln!(self.writer, "{}", symbol)?;
182                writeln!(self.writer, "Forces of Component {}", type_idx + 1)?;
183
184                for i in 0..num_atoms_in_type {
185                    let atom = &frame.atom_data[force_idx_offset + i];
186                    let [fx, fy, fz] = atom.force.unwrap_or([0.0; 3]);
187                    writeln!(
188                        self.writer,
189                        "{fx:.prec$} {fy:.prec$} {fz:.prec$} {fixed_flag} {atom_id}",
190                        prec = prec,
191                        fixed_flag = encode_fixed_bitmask(atom.fixed),
192                        atom_id = atom.atom_id
193                    )?;
194                }
195                force_idx_offset += num_atoms_in_type;
196            }
197        }
198
199        Ok(())
200    }
201
202    /// Writes all frames from an iterator to the output stream.
203    ///
204    /// This is the most convenient way to write a multi-frame file.
205    pub fn extend<'a>(&mut self, frames: impl Iterator<Item = &'a ConFrame>) -> io::Result<()> {
206        for frame in frames {
207            self.write_frame(frame)?;
208        }
209        Ok(())
210    }
211}
212
213// Implementation block specifically for when the writer is a `File`.
214impl ConFrameWriter<File> {
215    /// Creates a new `ConFrameWriter` that writes to a file at the given path.
216    ///
217    /// This is a convenience function that creates the file and wraps it.
218    pub fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
219        let file = File::create(path)?;
220        Ok(Self::new(file))
221    }
222
223    /// Creates a new `ConFrameWriter` that writes to a file with a custom precision.
224    pub fn from_path_with_precision<P: AsRef<Path>>(path: P, precision: usize) -> io::Result<Self> {
225        let file = File::create(path)?;
226        Ok(Self::with_precision(file, precision))
227    }
228}
229
230// Gzip-compressed writer constructors.
231impl ConFrameWriter<flate2::write::GzEncoder<File>> {
232    /// Creates a gzip-compressed writer for the given path.
233    pub fn from_path_gzip<P: AsRef<Path>>(path: P) -> io::Result<Self> {
234        let encoder = crate::compression::gzip_writer(path.as_ref())?;
235        Ok(Self::new(encoder))
236    }
237
238    /// Creates a gzip-compressed writer with custom precision.
239    pub fn from_path_gzip_with_precision<P: AsRef<Path>>(
240        path: P,
241        precision: usize,
242    ) -> io::Result<Self> {
243        let encoder = crate::compression::gzip_writer(path.as_ref())?;
244        Ok(Self::with_precision(encoder, precision))
245    }
246}