1use crate::types::{
2 ConFrame, SECTION_CHARGES, SECTION_DISPLACEMENTS, SECTION_ENERGIES, SECTION_FORCES,
3 SECTION_MAGMOMS, SECTION_SPINS, 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
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum FloatFormat {
16 DecimalPlaces(usize),
18 RoundTrip,
21}
22
23impl Default for FloatFormat {
24 fn default() -> Self {
25 Self::DecimalPlaces(DEFAULT_FLOAT_PRECISION)
26 }
27}
28
29pub struct ConFrameWriter<W: Write> {
44 writer: BufWriter<W>,
45 float_format: FloatFormat,
46 canonical: bool,
51 metadata_cache: Option<MetadataCacheEntry>,
58 scratch: Vec<u8>,
61}
62
63#[derive(Debug)]
64struct MetadataCacheEntry {
65 spec_version: u32,
69 has_velocities: bool,
70 has_forces: bool,
71 has_energies: bool,
72 has_charges: bool,
73 has_spins: bool,
74 has_magmoms: bool,
75 has_displacements: bool,
76 metadata: std::collections::BTreeMap<String, serde_json::Value>,
77 serialized: String,
79}
80
81impl MetadataCacheEntry {
82 fn matches(
83 &self,
84 spec_version: u32,
85 has_velocities: bool,
86 has_forces: bool,
87 has_energies: bool,
88 has_charges: bool,
89 has_spins: bool,
90 has_magmoms: bool,
91 has_displacements: bool,
92 metadata: &std::collections::BTreeMap<String, serde_json::Value>,
93 ) -> bool {
94 self.spec_version == spec_version
95 && self.has_velocities == has_velocities
96 && self.has_forces == has_forces
97 && self.has_energies == has_energies
98 && self.has_charges == has_charges
99 && self.has_spins == has_spins
100 && self.has_magmoms == has_magmoms
101 && self.has_displacements == has_displacements
102 && &self.metadata == metadata
103 }
104}
105
106impl<W: Write> ConFrameWriter<W> {
108 pub fn new(writer: W) -> Self {
114 Self::with_float_format(writer, FloatFormat::default())
115 }
116
117 pub fn with_precision(writer: W, precision: usize) -> Self {
124 Self::with_float_format(writer, FloatFormat::DecimalPlaces(precision))
125 }
126
127 pub fn with_float_format(writer: W, float_format: FloatFormat) -> Self {
132 Self {
133 writer: BufWriter::new(writer),
134 float_format,
135 canonical: false,
136 metadata_cache: None,
137 scratch: Vec::with_capacity(16 * 1024),
138 }
139 }
140
141 pub fn canonical(mut self, on: bool) -> Self {
146 self.set_canonical(on);
147 self
148 }
149
150 pub fn set_canonical(&mut self, on: bool) {
152 self.canonical = on;
153 if on {
154 self.metadata_cache = None;
155 }
156 }
157
158 pub fn is_canonical(&self) -> bool {
160 self.canonical
161 }
162
163 fn refresh_metadata_cache(&mut self, frame: &ConFrame) {
164 let spec_version = frame.header.spec_version;
165 let has_vel = frame.has_velocities();
166 let has_frc = frame.has_forces();
167 let has_eng = frame.has_energies();
168 let has_chg = frame.has_charges();
169 let has_spn = frame.has_spins();
170 let has_mm = frame.has_magmoms();
171 let has_dsp = frame.has_displacements();
172
173 let cache_hit = !self.canonical
174 && self.metadata_cache.as_ref().is_some_and(|c| {
175 c.matches(
176 spec_version,
177 has_vel,
178 has_frc,
179 has_eng,
180 has_chg,
181 has_spn,
182 has_mm,
183 has_dsp,
184 &frame.header.metadata,
185 )
186 });
187 if cache_hit {
188 return;
189 }
190
191 let mut meta_obj = serde_json::Map::new();
192 meta_obj.insert(meta::CON_SPEC_VERSION.into(), json!(spec_version));
193 let mut sections = Vec::new();
194 if has_vel {
195 sections.push(json!(SECTION_VELOCITIES));
196 }
197 if has_frc {
198 sections.push(json!(SECTION_FORCES));
199 }
200 if has_eng {
201 sections.push(json!(SECTION_ENERGIES));
202 }
203 if has_chg {
204 sections.push(json!(SECTION_CHARGES));
205 }
206 if has_spn {
207 sections.push(json!(SECTION_SPINS));
208 }
209 if has_mm {
210 sections.push(json!(SECTION_MAGMOMS));
211 }
212 if has_dsp {
213 sections.push(json!(SECTION_DISPLACEMENTS));
214 }
215 let validate = frame
216 .header
217 .metadata
218 .get(meta::VALIDATE)
219 .and_then(|value| value.as_bool())
220 .unwrap_or(false);
221 if !sections.is_empty() || validate {
222 meta_obj.insert(meta::SECTIONS.into(), json!(sections));
223 }
224 for (k, v) in &frame.header.metadata {
225 if k == meta::CON_SPEC_VERSION || k == meta::SECTIONS {
226 continue;
227 }
228 meta_obj.insert(k.clone(), v.clone());
229 }
230 if let Some(u) = meta_obj.get(meta::UNITS).cloned() {
231 if let Ok(c) = crate::units::canonicalize_units_object(&u) {
232 meta_obj.insert(meta::UNITS.into(), c);
233 }
234 }
235 if spec_version >= 3 {
236 let need_default = match meta_obj.get(meta::UNITS) {
237 None => true,
238 Some(u) => crate::units::validate_v3_units_metadata(u).is_err(),
239 };
240 if need_default {
241 meta_obj.insert(meta::UNITS.into(), crate::units::default_v3_units_json());
242 }
243 }
244 let serialized = serde_json::Value::Object(meta_obj).to_string();
245 self.metadata_cache = Some(MetadataCacheEntry {
246 spec_version,
247 has_velocities: has_vel,
248 has_forces: has_frc,
249 has_energies: has_eng,
250 has_charges: has_chg,
251 has_spins: has_spn,
252 has_magmoms: has_mm,
253 has_displacements: has_dsp,
254 metadata: frame.header.metadata.clone(),
255 serialized,
256 });
257 }
258
259 pub fn write_frame(&mut self, frame: &ConFrame) -> io::Result<()> {
261 let prec = self.float_format;
262 self.refresh_metadata_cache(frame);
263 let meta_line = self
264 .metadata_cache
265 .as_ref()
266 .expect("metadata_cache populated above")
267 .serialized
268 .clone();
269 self.scratch.clear();
270 {
271 let buf = &mut self.scratch;
272
273 let _ = writeln!(buf, "{}", frame.header.prebox_header.user);
275 let _ = writeln!(buf, "{meta_line}");
276 push_f64(buf, frame.header.boxl[0], prec);
277 buf.push(b' ');
278 push_f64(buf, frame.header.boxl[1], prec);
279 buf.push(b' ');
280 push_f64(buf, frame.header.boxl[2], prec);
281 buf.push(b'\n');
282 push_f64(buf, frame.header.angles[0], prec);
283 buf.push(b' ');
284 push_f64(buf, frame.header.angles[1], prec);
285 buf.push(b' ');
286 push_f64(buf, frame.header.angles[2], prec);
287 buf.push(b'\n');
288 let _ = writeln!(buf, "{}", frame.header.postbox_header[0]);
289 let _ = writeln!(buf, "{}", frame.header.postbox_header[1]);
290 let _ = writeln!(buf, "{}", frame.header.natm_types);
291
292 for (i, n) in frame.header.natms_per_type.iter().enumerate() {
293 if i > 0 {
294 buf.push(b' ');
295 }
296 push_u64(buf, *n as u64);
297 }
298 buf.push(b'\n');
299
300 for (i, m) in frame.header.masses_per_type.iter().enumerate() {
301 if i > 0 {
302 buf.push(b' ');
303 }
304 push_f64(buf, *m, prec);
305 }
306 buf.push(b'\n');
307
308 let mut atom_idx_offset = 0;
310 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
311 let symbol = &frame.atom_data[atom_idx_offset].symbol;
312 let _ = writeln!(buf, "{symbol}");
313 let _ = writeln!(buf, "Coordinates of Component {}", type_idx + 1);
314
315 for i in 0..num_atoms_in_type {
316 let atom = &frame.atom_data[atom_idx_offset + i];
317 push_xyz_line(
318 buf,
319 atom.x,
320 atom.y,
321 atom.z,
322 prec,
323 encode_fixed_bitmask(atom.fixed),
324 atom.atom_id,
325 );
326 }
327 atom_idx_offset += num_atoms_in_type;
328 }
329
330 if frame.has_velocities() {
332 buf.push(b'\n');
333
334 let mut vel_idx_offset = 0;
335 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
336 {
337 let symbol = &frame.atom_data[vel_idx_offset].symbol;
338 let _ = writeln!(buf, "{symbol}");
339 let _ = writeln!(buf, "Velocities of Component {}", type_idx + 1);
340
341 for i in 0..num_atoms_in_type {
342 let atom = &frame.atom_data[vel_idx_offset + i];
343 let [vx, vy, vz] = atom.velocity.unwrap_or([0.0; 3]);
344 push_xyz_line(
345 buf,
346 vx,
347 vy,
348 vz,
349 prec,
350 encode_fixed_bitmask(atom.fixed),
351 atom.atom_id,
352 );
353 }
354 vel_idx_offset += num_atoms_in_type;
355 }
356 }
357
358 if frame.has_forces() {
360 buf.push(b'\n');
361
362 let mut force_idx_offset = 0;
363 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
364 {
365 let symbol = &frame.atom_data[force_idx_offset].symbol;
366 let _ = writeln!(buf, "{symbol}");
367 let _ = writeln!(buf, "Forces of Component {}", type_idx + 1);
368
369 for i in 0..num_atoms_in_type {
370 let atom = &frame.atom_data[force_idx_offset + i];
371 let [fx, fy, fz] = atom.force.unwrap_or([0.0; 3]);
372 push_xyz_line(
373 buf,
374 fx,
375 fy,
376 fz,
377 prec,
378 encode_fixed_bitmask(atom.fixed),
379 atom.atom_id,
380 );
381 }
382 force_idx_offset += num_atoms_in_type;
383 }
384 }
385
386 if frame.has_energies() {
388 buf.push(b'\n');
389
390 let mut energy_idx_offset = 0;
391 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
392 {
393 let symbol = &frame.atom_data[energy_idx_offset].symbol;
394 let _ = writeln!(buf, "{symbol}");
395 let _ = writeln!(buf, "Energies of Component {}", type_idx + 1);
396
397 for i in 0..num_atoms_in_type {
398 let atom = &frame.atom_data[energy_idx_offset + i];
399 let e = atom.energy.unwrap_or(0.0);
400 push_scalar_line(
401 buf,
402 e,
403 prec,
404 encode_fixed_bitmask(atom.fixed),
405 atom.atom_id,
406 );
407 }
408 energy_idx_offset += num_atoms_in_type;
409 }
410 }
411
412 if frame.has_charges() {
413 buf.push(b'\n');
414 let mut off = 0;
415 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
416 {
417 let symbol = &frame.atom_data[off].symbol;
418 let _ = writeln!(buf, "{symbol}");
419 let _ = writeln!(buf, "Charges of Component {}", type_idx + 1);
420 for i in 0..num_atoms_in_type {
421 let atom = &frame.atom_data[off + i];
422 let q = atom.charge.unwrap_or(0.0);
423 push_scalar_line(
424 buf,
425 q,
426 prec,
427 encode_fixed_bitmask(atom.fixed),
428 atom.atom_id,
429 );
430 }
431 off += num_atoms_in_type;
432 }
433 }
434
435 if frame.has_spins() {
436 buf.push(b'\n');
437 let mut off = 0;
438 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
439 {
440 let symbol = &frame.atom_data[off].symbol;
441 let _ = writeln!(buf, "{symbol}");
442 let _ = writeln!(buf, "Spins of Component {}", type_idx + 1);
443 for i in 0..num_atoms_in_type {
444 let atom = &frame.atom_data[off + i];
445 let s = atom.spin.unwrap_or(0.0);
446 push_scalar_line(
447 buf,
448 s,
449 prec,
450 encode_fixed_bitmask(atom.fixed),
451 atom.atom_id,
452 );
453 }
454 off += num_atoms_in_type;
455 }
456 }
457
458 if frame.has_magmoms() {
459 buf.push(b'\n');
460 let mut off = 0;
461 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
462 {
463 let symbol = &frame.atom_data[off].symbol;
464 let _ = writeln!(buf, "{symbol}");
465 let _ = writeln!(buf, "Magmoms of Component {}", type_idx + 1);
466 for i in 0..num_atoms_in_type {
467 let atom = &frame.atom_data[off + i];
468 let [mx, my, mz] = atom.magmom.unwrap_or([0.0; 3]);
469 push_xyz_line(
470 buf,
471 mx,
472 my,
473 mz,
474 prec,
475 encode_fixed_bitmask(atom.fixed),
476 atom.atom_id,
477 );
478 }
479 off += num_atoms_in_type;
480 }
481 }
482
483 if frame.has_displacements() {
484 buf.push(b'\n');
485 let mut off = 0;
486 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
487 {
488 let symbol = &frame.atom_data[off].symbol;
489 let _ = writeln!(buf, "{symbol}");
490 let _ = writeln!(buf, "Displacements of Component {}", type_idx + 1);
491 for i in 0..num_atoms_in_type {
492 let atom = &frame.atom_data[off + i];
493 let [dx, dy, dz] = atom.displacement.unwrap_or([0.0; 3]);
494 push_xyz_line(
495 buf,
496 dx,
497 dy,
498 dz,
499 prec,
500 encode_fixed_bitmask(atom.fixed),
501 atom.atom_id,
502 );
503 }
504 off += num_atoms_in_type;
505 }
506 }
507 }
508
509 self.writer.write_all(&self.scratch)
510 }
511
512 pub fn extend<'a>(&mut self, frames: impl Iterator<Item = &'a ConFrame>) -> io::Result<()> {
516 for frame in frames {
517 self.write_frame(frame)?;
518 }
519 Ok(())
520 }
521}
522
523fn push_u64(buf: &mut Vec<u8>, n: u64) {
524 push_u128(buf, u128::from(n));
525}
526
527fn push_u128(buf: &mut Vec<u8>, mut n: u128) {
528 let mut tmp = [0u8; 40];
529 let mut i = 40;
530 if n == 0 {
531 buf.push(b'0');
532 return;
533 }
534 while n > 0 {
535 i -= 1;
536 tmp[i] = b'0' + (n % 10) as u8;
537 n /= 10;
538 }
539 buf.extend_from_slice(&tmp[i..]);
540}
541
542fn push_f64(buf: &mut Vec<u8>, value: f64, format: FloatFormat) {
543 match format {
544 FloatFormat::DecimalPlaces(precision) => push_f64_prec(buf, value, precision),
545 FloatFormat::RoundTrip => {
546 let _ = write!(buf, "{value:e}");
547 }
548 }
549}
550
551fn push_f64_prec(buf: &mut Vec<u8>, v: f64, prec: usize) {
554 if !v.is_finite() || prec != 6 {
557 let _ = write!(buf, "{v:.prec$}");
558 return;
559 }
560 if v.is_sign_negative() {
561 buf.push(b'-');
562 }
563 let ax = v.abs();
564 if prec == 0 {
565 push_u128(buf, ax.round() as u128);
566 return;
567 }
568 let scale = 10u128.pow(prec as u32);
569 let n = (ax * scale as f64).round() as u128;
570 let int_part = n / scale;
571 let frac = n % scale;
572 push_u128(buf, int_part);
573 buf.push(b'.');
574 let mut tmp = [b'0'; 20];
575 let mut x = frac;
576 let mut i = prec;
577 while i > 0 {
578 i -= 1;
579 tmp[i] = b'0' + (x % 10) as u8;
580 x /= 10;
581 }
582 buf.extend_from_slice(&tmp[..prec]);
583}
584
585fn push_xyz_line(
586 buf: &mut Vec<u8>,
587 x: f64,
588 y: f64,
589 z: f64,
590 prec: FloatFormat,
591 fixed: u8,
592 atom_id: u64,
593) {
594 push_f64(buf, x, prec);
595 buf.push(b' ');
596 push_f64(buf, y, prec);
597 buf.push(b' ');
598 push_f64(buf, z, prec);
599 buf.push(b' ');
600 push_u64(buf, u64::from(fixed));
601 buf.push(b' ');
602 push_u64(buf, atom_id);
603 buf.push(b'\n');
604}
605
606fn push_scalar_line(buf: &mut Vec<u8>, v: f64, prec: FloatFormat, fixed: u8, atom_id: u64) {
607 push_f64(buf, v, prec);
608 buf.push(b' ');
609 push_u64(buf, u64::from(fixed));
610 buf.push(b' ');
611 push_u64(buf, atom_id);
612 buf.push(b'\n');
613}
614
615impl ConFrameWriter<File> {
617 pub fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
621 let file = File::create(path)?;
622 Ok(Self::new(file))
623 }
624
625 pub fn from_path_with_precision<P: AsRef<Path>>(path: P, precision: usize) -> io::Result<Self> {
627 let file = File::create(path)?;
628 Ok(Self::with_precision(file, precision))
629 }
630}
631
632impl ConFrameWriter<flate2::write::GzEncoder<File>> {
634 pub fn from_path_gzip<P: AsRef<Path>>(path: P) -> io::Result<Self> {
636 let encoder = crate::compression::gzip_writer(path.as_ref())?;
637 Ok(Self::new(encoder))
638 }
639
640 pub fn from_path_gzip_with_precision<P: AsRef<Path>>(
642 path: P,
643 precision: usize,
644 ) -> io::Result<Self> {
645 let encoder = crate::compression::gzip_writer(path.as_ref())?;
646 Ok(Self::with_precision(encoder, precision))
647 }
648}
649
650#[cfg(feature = "zstd")]
653impl ConFrameWriter<zstd::stream::write::AutoFinishEncoder<'static, File>> {
654 pub fn from_path_zstd<P: AsRef<Path>>(path: P) -> io::Result<Self> {
656 let encoder = crate::compression::zstd_writer(path.as_ref())?;
657 Ok(Self::new(encoder))
658 }
659
660 pub fn from_path_zstd_with_precision<P: AsRef<Path>>(
662 path: P,
663 precision: usize,
664 ) -> io::Result<Self> {
665 let encoder = crate::compression::zstd_writer(path.as_ref())?;
666 Ok(Self::with_precision(encoder, precision))
667 }
668}
669
670#[cfg(test)]
671mod float_format_tests {
672 use super::push_f64_prec;
673
674 fn formatted(v: f64, prec: usize) -> String {
675 let mut buf = Vec::new();
676 push_f64_prec(&mut buf, v, prec);
677 String::from_utf8(buf).expect("utf8")
678 }
679
680 #[test]
681 fn matches_std_fixed_precision() {
682 let vals = [
683 0.0,
684 -0.0,
685 1.0,
686 -1.0,
687 0.9045,
688 6.975_299_999_999_995,
689 63.546,
690 1.008,
691 15.3456,
692 90.0,
693 218.0,
694 1e-6,
695 -1.23456789,
696 10.0,
697 ];
698 for prec in [0usize, 6] {
699 for v in vals {
700 let got = formatted(v, prec);
701 let exp = format!("{v:.prec$}");
702 assert_eq!(got, exp, "v={v:?} prec={prec}");
703 }
704 }
705 }
706}