1use crate::types::{
2 ConFrame, SECTION_CHARGES, SECTION_ENERGIES, SECTION_FORCES, SECTION_MAGMOMS, SECTION_SPINS,
3 SECTION_VELOCITIES, encode_fixed_bitmask, meta,
4};
5use serde_json::json;
6use std::fs::File;
7use std::io::{self, BufWriter, Write};
8use std::path::Path;
9
10const DEFAULT_FLOAT_PRECISION: usize = 6;
12
13pub struct ConFrameWriter<W: Write> {
28 writer: BufWriter<W>,
29 precision: usize,
30 canonical: bool,
35 metadata_cache: Option<MetadataCacheEntry>,
42}
43
44#[derive(Debug)]
45struct MetadataCacheEntry {
46 spec_version: u32,
50 has_velocities: bool,
51 has_forces: bool,
52 has_energies: bool,
53 has_charges: bool,
54 has_spins: bool,
55 has_magmoms: bool,
56 metadata: std::collections::BTreeMap<String, serde_json::Value>,
57 serialized: String,
59}
60
61impl MetadataCacheEntry {
62 fn matches(
63 &self,
64 spec_version: u32,
65 has_velocities: bool,
66 has_forces: bool,
67 has_energies: bool,
68 has_charges: bool,
69 has_spins: bool,
70 has_magmoms: bool,
71 metadata: &std::collections::BTreeMap<String, serde_json::Value>,
72 ) -> bool {
73 self.spec_version == spec_version
74 && self.has_velocities == has_velocities
75 && self.has_forces == has_forces
76 && self.has_energies == has_energies
77 && self.has_charges == has_charges
78 && self.has_spins == has_spins
79 && self.has_magmoms == has_magmoms
80 && &self.metadata == metadata
81 }
82}
83
84impl<W: Write> ConFrameWriter<W> {
86 pub fn new(writer: W) -> Self {
92 Self {
93 writer: BufWriter::new(writer),
94 precision: DEFAULT_FLOAT_PRECISION,
95 canonical: false,
96 metadata_cache: None,
97 }
98 }
99
100 pub fn with_precision(writer: W, precision: usize) -> Self {
107 Self {
108 writer: BufWriter::new(writer),
109 precision,
110 canonical: false,
111 metadata_cache: None,
112 }
113 }
114
115 pub fn canonical(mut self, on: bool) -> Self {
120 self.set_canonical(on);
121 self
122 }
123
124 pub fn set_canonical(&mut self, on: bool) {
126 self.canonical = on;
127 if on {
128 self.metadata_cache = None;
129 }
130 }
131
132 pub fn is_canonical(&self) -> bool {
134 self.canonical
135 }
136
137 pub fn write_frame(&mut self, frame: &ConFrame) -> io::Result<()> {
139 let prec = self.precision;
140
141 writeln!(self.writer, "{}", frame.header.prebox_header.user)?;
143
144 let spec_version = frame.header.spec_version;
152 let has_vel = frame.has_velocities();
153 let has_frc = frame.has_forces();
154 let has_eng = frame.has_energies();
155 let has_chg = frame.has_charges();
156 let has_spn = frame.has_spins();
157 let has_mm = frame.has_magmoms();
158
159 let cache_hit = !self.canonical
160 && self.metadata_cache.as_ref().is_some_and(|c| {
161 c.matches(
162 spec_version,
163 has_vel,
164 has_frc,
165 has_eng,
166 has_chg,
167 has_spn,
168 has_mm,
169 &frame.header.metadata,
170 )
171 });
172
173 if !cache_hit {
174 let mut meta_obj = serde_json::Map::new();
175 meta_obj.insert(
176 meta::CON_SPEC_VERSION.into(),
177 json!(spec_version),
178 );
179 let mut sections = Vec::new();
180 if has_vel {
181 sections.push(json!(SECTION_VELOCITIES));
182 }
183 if has_frc {
184 sections.push(json!(SECTION_FORCES));
185 }
186 if has_eng {
187 sections.push(json!(SECTION_ENERGIES));
188 }
189 if has_chg {
190 sections.push(json!(SECTION_CHARGES));
191 }
192 if has_spn {
193 sections.push(json!(SECTION_SPINS));
194 }
195 if has_mm {
196 sections.push(json!(SECTION_MAGMOMS));
197 }
198 let validate = frame
199 .header
200 .metadata
201 .get(meta::VALIDATE)
202 .and_then(|value| value.as_bool())
203 .unwrap_or(false);
204 if !sections.is_empty() || validate {
205 meta_obj.insert(meta::SECTIONS.into(), json!(sections));
206 }
207 for (k, v) in &frame.header.metadata {
209 if k == meta::CON_SPEC_VERSION || k == meta::SECTIONS {
210 continue;
211 }
212 meta_obj.insert(k.clone(), v.clone());
213 }
214 if spec_version >= 3 {
216 let need_default = match meta_obj.get(meta::UNITS) {
217 None => true,
218 Some(u) => crate::units::validate_v3_units_metadata(u).is_err(),
219 };
220 if need_default {
221 meta_obj.insert(
222 meta::UNITS.into(),
223 crate::units::default_v3_units_json(),
224 );
225 }
226 }
227 let serialized = serde_json::Value::Object(meta_obj).to_string();
229 self.metadata_cache = Some(MetadataCacheEntry {
230 spec_version,
231 has_velocities: has_vel,
232 has_forces: has_frc,
233 has_energies: has_eng,
234 has_charges: has_chg,
235 has_spins: has_spn,
236 has_magmoms: has_mm,
237 metadata: frame.header.metadata.clone(),
238 serialized,
239 });
240 }
241
242 let cached = self
243 .metadata_cache
244 .as_ref()
245 .expect("metadata_cache populated above");
246 writeln!(self.writer, "{}", cached.serialized)?;
247 writeln!(
248 self.writer,
249 "{1:.0$} {2:.0$} {3:.0$}",
250 prec, frame.header.boxl[0], frame.header.boxl[1], frame.header.boxl[2]
251 )?;
252 writeln!(
253 self.writer,
254 "{1:.0$} {2:.0$} {3:.0$}",
255 prec, frame.header.angles[0], frame.header.angles[1], frame.header.angles[2]
256 )?;
257 writeln!(self.writer, "{}", frame.header.postbox_header[0])?;
258 writeln!(self.writer, "{}", frame.header.postbox_header[1])?;
259 writeln!(self.writer, "{}", frame.header.natm_types)?;
260
261 let natms_str: Vec<String> = frame
262 .header
263 .natms_per_type
264 .iter()
265 .map(|n| n.to_string())
266 .collect();
267 writeln!(self.writer, "{}", natms_str.join(" "))?;
268
269 let masses_str: Vec<String> = frame
270 .header
271 .masses_per_type
272 .iter()
273 .map(|m| format!("{:.1$}", m, prec))
274 .collect();
275 writeln!(self.writer, "{}", masses_str.join(" "))?;
276
277 let mut atom_idx_offset = 0;
279 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
280 let symbol = &frame.atom_data[atom_idx_offset].symbol;
281 writeln!(self.writer, "{}", symbol)?;
282 writeln!(self.writer, "Coordinates of Component {}", type_idx + 1)?;
283
284 for i in 0..num_atoms_in_type {
285 let atom = &frame.atom_data[atom_idx_offset + i];
286 writeln!(
287 self.writer,
288 "{x:.prec$} {y:.prec$} {z:.prec$} {fixed_flag} {atom_id}",
289 prec = prec,
290 x = atom.x,
291 y = atom.y,
292 z = atom.z,
293 fixed_flag = encode_fixed_bitmask(atom.fixed),
294 atom_id = atom.atom_id
295 )?;
296 }
297 atom_idx_offset += num_atoms_in_type;
298 }
299
300 if frame.has_velocities() {
302 writeln!(self.writer)?;
304
305 let mut vel_idx_offset = 0;
306 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
307 let symbol = &frame.atom_data[vel_idx_offset].symbol;
308 writeln!(self.writer, "{}", symbol)?;
309 writeln!(self.writer, "Velocities of Component {}", type_idx + 1)?;
310
311 for i in 0..num_atoms_in_type {
312 let atom = &frame.atom_data[vel_idx_offset + i];
313 let [vx, vy, vz] = atom.velocity.unwrap_or([0.0; 3]);
314 writeln!(
315 self.writer,
316 "{vx:.prec$} {vy:.prec$} {vz:.prec$} {fixed_flag} {atom_id}",
317 prec = prec,
318 fixed_flag = encode_fixed_bitmask(atom.fixed),
319 atom_id = atom.atom_id
320 )?;
321 }
322 vel_idx_offset += num_atoms_in_type;
323 }
324 }
325
326 if frame.has_forces() {
328 writeln!(self.writer)?;
330
331 let mut force_idx_offset = 0;
332 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
333 let symbol = &frame.atom_data[force_idx_offset].symbol;
334 writeln!(self.writer, "{}", symbol)?;
335 writeln!(self.writer, "Forces of Component {}", type_idx + 1)?;
336
337 for i in 0..num_atoms_in_type {
338 let atom = &frame.atom_data[force_idx_offset + i];
339 let [fx, fy, fz] = atom.force.unwrap_or([0.0; 3]);
340 writeln!(
341 self.writer,
342 "{fx:.prec$} {fy:.prec$} {fz:.prec$} {fixed_flag} {atom_id}",
343 prec = prec,
344 fixed_flag = encode_fixed_bitmask(atom.fixed),
345 atom_id = atom.atom_id
346 )?;
347 }
348 force_idx_offset += num_atoms_in_type;
349 }
350 }
351
352 if frame.has_energies() {
354 writeln!(self.writer)?;
355
356 let mut energy_idx_offset = 0;
357 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
358 let symbol = &frame.atom_data[energy_idx_offset].symbol;
359 writeln!(self.writer, "{}", symbol)?;
360 writeln!(self.writer, "Energies of Component {}", type_idx + 1)?;
361
362 for i in 0..num_atoms_in_type {
363 let atom = &frame.atom_data[energy_idx_offset + i];
364 let e = atom.energy.unwrap_or(0.0);
365 writeln!(
366 self.writer,
367 "{e:.prec$} {fixed_flag} {atom_id}",
368 prec = prec,
369 fixed_flag = encode_fixed_bitmask(atom.fixed),
370 atom_id = atom.atom_id
371 )?;
372 }
373 energy_idx_offset += num_atoms_in_type;
374 }
375 }
376
377 if frame.has_charges() {
378 writeln!(self.writer)?;
379 let mut off = 0;
380 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
381 let symbol = &frame.atom_data[off].symbol;
382 writeln!(self.writer, "{}", symbol)?;
383 writeln!(self.writer, "Charges of Component {}", type_idx + 1)?;
384 for i in 0..num_atoms_in_type {
385 let atom = &frame.atom_data[off + i];
386 let q = atom.charge.unwrap_or(0.0);
387 writeln!(
388 self.writer,
389 "{q:.prec$} {fixed_flag} {atom_id}",
390 prec = prec,
391 fixed_flag = encode_fixed_bitmask(atom.fixed),
392 atom_id = atom.atom_id
393 )?;
394 }
395 off += num_atoms_in_type;
396 }
397 }
398
399 if frame.has_spins() {
400 writeln!(self.writer)?;
401 let mut off = 0;
402 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
403 let symbol = &frame.atom_data[off].symbol;
404 writeln!(self.writer, "{}", symbol)?;
405 writeln!(self.writer, "Spins of Component {}", type_idx + 1)?;
406 for i in 0..num_atoms_in_type {
407 let atom = &frame.atom_data[off + i];
408 let s = atom.spin.unwrap_or(0.0);
409 writeln!(
410 self.writer,
411 "{s:.prec$} {fixed_flag} {atom_id}",
412 prec = prec,
413 fixed_flag = encode_fixed_bitmask(atom.fixed),
414 atom_id = atom.atom_id
415 )?;
416 }
417 off += num_atoms_in_type;
418 }
419 }
420
421 if frame.has_magmoms() {
422 writeln!(self.writer)?;
423 let mut off = 0;
424 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
425 let symbol = &frame.atom_data[off].symbol;
426 writeln!(self.writer, "{}", symbol)?;
427 writeln!(self.writer, "Magmoms of Component {}", type_idx + 1)?;
428 for i in 0..num_atoms_in_type {
429 let atom = &frame.atom_data[off + i];
430 let [mx, my, mz] = atom.magmom.unwrap_or([0.0; 3]);
431 writeln!(
432 self.writer,
433 "{mx:.prec$} {my:.prec$} {mz:.prec$} {fixed_flag} {atom_id}",
434 prec = prec,
435 fixed_flag = encode_fixed_bitmask(atom.fixed),
436 atom_id = atom.atom_id
437 )?;
438 }
439 off += num_atoms_in_type;
440 }
441 }
442
443 Ok(())
444 }
445
446 pub fn extend<'a>(&mut self, frames: impl Iterator<Item = &'a ConFrame>) -> io::Result<()> {
450 for frame in frames {
451 self.write_frame(frame)?;
452 }
453 Ok(())
454 }
455}
456
457impl ConFrameWriter<File> {
459 pub fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
463 let file = File::create(path)?;
464 Ok(Self::new(file))
465 }
466
467 pub fn from_path_with_precision<P: AsRef<Path>>(path: P, precision: usize) -> io::Result<Self> {
469 let file = File::create(path)?;
470 Ok(Self::with_precision(file, precision))
471 }
472}
473
474impl ConFrameWriter<flate2::write::GzEncoder<File>> {
476 pub fn from_path_gzip<P: AsRef<Path>>(path: P) -> io::Result<Self> {
478 let encoder = crate::compression::gzip_writer(path.as_ref())?;
479 Ok(Self::new(encoder))
480 }
481
482 pub fn from_path_gzip_with_precision<P: AsRef<Path>>(
484 path: P,
485 precision: usize,
486 ) -> io::Result<Self> {
487 let encoder = crate::compression::gzip_writer(path.as_ref())?;
488 Ok(Self::with_precision(encoder, precision))
489 }
490}
491
492#[cfg(feature = "zstd")]
495impl ConFrameWriter<zstd::stream::write::AutoFinishEncoder<'static, File>> {
496 pub fn from_path_zstd<P: AsRef<Path>>(path: P) -> io::Result<Self> {
498 let encoder = crate::compression::zstd_writer(path.as_ref())?;
499 Ok(Self::new(encoder))
500 }
501
502 pub fn from_path_zstd_with_precision<P: AsRef<Path>>(
504 path: P,
505 precision: usize,
506 ) -> io::Result<Self> {
507 let encoder = crate::compression::zstd_writer(path.as_ref())?;
508 Ok(Self::with_precision(encoder, precision))
509 }
510}