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