1use crate::types::{
2 ConFrame, ConFrameBuilder, SECTION_CHARGES, SECTION_DISPLACEMENTS, SECTION_ENERGIES,
3 SECTION_FORCES, SECTION_MAGMOMS, SECTION_SPINS, SECTION_SPREADS, SECTION_VELOCITIES,
4 encode_fixed_bitmask, meta,
5};
6use serde_json::{Value, json};
7use std::collections::BTreeMap;
8use std::fs::File;
9use std::io::{self, BufWriter, Write};
10use std::path::Path;
11
12#[derive(Debug)]
15pub enum WriteError {
16 Io(io::Error),
17 Format(crate::error::ParseError),
18}
19
20impl std::fmt::Display for WriteError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 WriteError::Io(e) => write!(f, "{e}"),
24 WriteError::Format(e) => write!(f, "{e}"),
25 }
26 }
27}
28
29impl std::error::Error for WriteError {
30 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
31 match self {
32 WriteError::Io(e) => Some(e),
33 WriteError::Format(e) => Some(e),
34 }
35 }
36}
37
38impl From<io::Error> for WriteError {
39 fn from(e: io::Error) -> Self {
40 WriteError::Io(e)
41 }
42}
43
44impl From<WriteError> for io::Error {
45 fn from(e: WriteError) -> Self {
46 match e {
47 WriteError::Io(e) => e,
48 WriteError::Format(err) => io::Error::new(io::ErrorKind::InvalidData, err.to_string()),
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum FloatFormat {
56 DecimalPlaces(usize),
58 RoundTrip,
61}
62
63impl Default for FloatFormat {
64 fn default() -> Self {
65 Self::RoundTrip
67 }
68}
69
70pub struct ConFrameWriter<W: Write> {
85 writer: BufWriter<W>,
86 float_format: FloatFormat,
87 canonical: bool,
91 metadata_cache: Option<MetadataCacheEntry>,
98 scratch: Vec<u8>,
101 ryu: ryu::Buffer,
103}
104
105#[derive(Debug)]
106struct MetadataCacheEntry {
107 spec_version: u32,
111 has_velocities: bool,
112 has_forces: bool,
113 has_energies: bool,
114 has_charges: bool,
115 has_spins: bool,
116 has_magmoms: bool,
117 has_displacements: bool,
118 has_spreads: bool,
119 metadata: std::collections::BTreeMap<String, serde_json::Value>,
120 serialized: String,
122}
123
124impl MetadataCacheEntry {
125 fn matches(
126 &self,
127 spec_version: u32,
128 has_velocities: bool,
129 has_forces: bool,
130 has_energies: bool,
131 has_charges: bool,
132 has_spins: bool,
133 has_magmoms: bool,
134 has_displacements: bool,
135 has_spreads: bool,
136 metadata: &std::collections::BTreeMap<String, serde_json::Value>,
137 ) -> bool {
138 self.spec_version == spec_version
139 && self.has_velocities == has_velocities
140 && self.has_forces == has_forces
141 && self.has_energies == has_energies
142 && self.has_charges == has_charges
143 && self.has_spins == has_spins
144 && self.has_magmoms == has_magmoms
145 && self.has_displacements == has_displacements
146 && self.has_spreads == has_spreads
147 && self.metadata.len() == metadata.len()
148 && self
149 .metadata
150 .iter()
151 .zip(metadata)
152 .all(|((ka, a), (kb, b))| ka == kb && metadata_value_eq(a, b))
153 }
154}
155
156fn metadata_value_eq(a: &serde_json::Value, b: &serde_json::Value) -> bool {
159 use serde_json::Value;
160 match (a, b) {
161 (Value::Number(a), Value::Number(b)) if a.is_f64() && b.is_f64() => {
162 a.as_f64().map(f64::to_bits) == b.as_f64().map(f64::to_bits)
163 }
164 (Value::Array(a), Value::Array(b)) => {
165 a.len() == b.len() && a.iter().zip(b).all(|(a, b)| metadata_value_eq(a, b))
166 }
167 (Value::Object(a), Value::Object(b)) => {
168 a.len() == b.len()
169 && a.iter()
170 .all(|(key, a)| b.get(key).is_some_and(|b| metadata_value_eq(a, b)))
171 }
172 _ => a == b,
173 }
174}
175
176fn require_minor2_section_arity(
179 minor: u64,
180 sections: &[serde_json::Value],
181 metadata: &serde_json::Map<String, serde_json::Value>,
182) -> Result<(), WriteError> {
183 if minor < 2 {
184 return Ok(());
185 }
186 for name in sections.iter().filter_map(|value| value.as_str()) {
187 if crate::types::reserved_section_value_arity(name).is_some() {
188 continue;
189 }
190 let listed = metadata
191 .get(meta::SECTION_ARITY)
192 .and_then(|value| value.as_object())
193 .and_then(|obj| obj.get(name))
194 .and_then(|value| value.as_u64())
195 .is_some_and(|n| n >= 1);
196 if !listed {
197 return Err(WriteError::Format(
198 crate::error::ParseError::ValidationError(format!(
199 "minor >= 2 must list {name} in section_arity"
200 )),
201 ));
202 }
203 }
204 Ok(())
205}
206
207impl<W: Write> ConFrameWriter<W> {
209 pub fn new(writer: W) -> Self {
215 Self::with_float_format(writer, FloatFormat::default())
216 }
217
218 pub fn with_precision(writer: W, precision: usize) -> Self {
225 Self::with_float_format(writer, FloatFormat::DecimalPlaces(precision))
226 }
227
228 pub fn with_float_format(writer: W, float_format: FloatFormat) -> Self {
233 Self {
234 writer: BufWriter::new(writer),
235 float_format,
236 canonical: false,
237 metadata_cache: None,
238 scratch: Vec::with_capacity(16 * 1024),
239 ryu: ryu::Buffer::new(),
240 }
241 }
242
243 pub fn canonical(mut self, on: bool) -> Self {
248 self.set_canonical(on);
249 self
250 }
251
252 pub fn set_canonical(&mut self, on: bool) {
254 self.canonical = on;
255 if on {
256 self.metadata_cache = None;
257 }
258 }
259
260 pub fn is_canonical(&self) -> bool {
262 self.canonical
263 }
264
265 fn refresh_metadata_cache(&mut self, frame: &ConFrame) -> Result<(), WriteError> {
266 let spec_version = frame.header.spec_version;
267 let has_vel = frame.has_velocities();
268 let has_frc = frame.has_forces();
269 let has_eng = frame.has_energies();
270 let has_chg = frame.has_charges();
271 let has_spn = frame.has_spins();
272 let has_mm = frame.has_magmoms();
273 let has_dsp = frame.has_displacements();
274 let has_spr = frame.has_spreads();
275
276 let cache_hit = !self.canonical
277 && self.metadata_cache.as_ref().is_some_and(|c| {
278 c.matches(
279 spec_version,
280 has_vel,
281 has_frc,
282 has_eng,
283 has_chg,
284 has_spn,
285 has_mm,
286 has_dsp,
287 has_spr,
288 &frame.header.metadata,
289 )
290 });
291 if cache_hit {
292 return Ok(());
293 }
294
295 let mut meta_obj = serde_json::Map::new();
296 meta_obj.insert(meta::CON_SPEC_VERSION.into(), json!(spec_version));
297 let mut sections = Vec::new();
298 if has_vel {
299 sections.push(json!(SECTION_VELOCITIES));
300 }
301 if has_frc {
302 sections.push(json!(SECTION_FORCES));
303 }
304 if has_eng {
305 sections.push(json!(SECTION_ENERGIES));
306 }
307 if has_chg {
308 sections.push(json!(SECTION_CHARGES));
309 }
310 if has_spn {
311 sections.push(json!(SECTION_SPINS));
312 }
313 if has_mm {
314 sections.push(json!(SECTION_MAGMOMS));
315 }
316 if has_dsp {
317 sections.push(json!(SECTION_DISPLACEMENTS));
318 }
319 if has_spr {
320 sections.push(json!(SECTION_SPREADS));
321 }
322 let validate = frame
323 .header
324 .metadata
325 .get(meta::VALIDATE)
326 .and_then(|value| value.as_bool())
327 .unwrap_or(false);
328 if !sections.is_empty() || validate {
329 meta_obj.insert(meta::SECTIONS.into(), json!(sections));
330 }
331 for (k, v) in &frame.header.metadata {
332 if k == meta::CON_SPEC_VERSION || k == meta::SECTIONS {
333 continue;
334 }
335 meta_obj.insert(k.clone(), v.clone());
336 }
337 if let Some(u) = meta_obj.get(meta::UNITS).cloned() {
338 let c = crate::units::canonicalize_units_object(&u).map_err(|e| {
340 WriteError::Format(crate::error::ParseError::InvalidUnits(e.to_string()))
341 })?;
342 meta_obj.insert(meta::UNITS.into(), c);
343 }
344 if spec_version >= 3 {
345 match meta_obj.get(meta::UNITS) {
346 None => {
347 return Err(WriteError::Format(crate::error::ParseError::InvalidUnits(
348 "con_spec_version >= 3 requires units.length and units.energy".into(),
349 )));
350 }
351 Some(u) => {
352 crate::units::validate_v3_units_metadata(u).map_err(|e| {
353 WriteError::Format(crate::error::ParseError::InvalidUnits(e.to_string()))
354 })?;
355 }
356 }
357 }
358 let minor = frame
359 .header
360 .metadata
361 .get(meta::CON_SPEC_MINOR)
362 .and_then(|v| v.as_u64())
363 .unwrap_or(0);
364 if spec_version >= 3 && minor >= 1 && has_vel {
365 let time_ok = meta_obj
366 .get(meta::UNITS)
367 .and_then(|u| u.get("time"))
368 .and_then(|v| v.as_str())
369 .is_some_and(|s| !s.trim().is_empty());
370 if !time_ok {
371 return Err(WriteError::Format(crate::error::ParseError::InvalidUnits(
372 "minor >= 1 requires units.time when a velocities section is present".into(),
373 )));
374 }
375 }
376 require_minor2_section_arity(minor, §ions, &meta_obj)?;
379 let serialized = serde_json::Value::Object(meta_obj).to_string();
380 self.metadata_cache = Some(MetadataCacheEntry {
381 spec_version,
382 has_velocities: has_vel,
383 has_forces: has_frc,
384 has_energies: has_eng,
385 has_charges: has_chg,
386 has_spins: has_spn,
387 has_magmoms: has_mm,
388 has_displacements: has_dsp,
389 has_spreads: has_spr,
390 metadata: frame.header.metadata.clone(),
391 serialized,
392 });
393 Ok(())
394 }
395
396 pub fn write_frame(&mut self, frame: &ConFrame) -> Result<(), WriteError> {
398 let zero_frame = frame.header.natm_types == 0
399 && frame.atom_data.is_empty()
400 && frame.header.natms_per_type.is_empty()
401 && frame.header.masses_per_type.is_empty();
402 if zero_frame && !frame.header.sections.is_empty() {
403 return Err(WriteError::Format(crate::error::ParseError::InvalidCount(
404 "a zero-atom frame cannot declare sections".into(),
405 )));
406 }
407 if !zero_frame
408 && (frame.header.natm_types == 0
409 || frame.atom_data.is_empty()
410 || frame.header.natms_per_type.iter().any(|n| *n == 0))
411 {
412 return Err(WriteError::Format(crate::error::ParseError::InvalidCount(
413 "each per-type count must be >= 1".into(),
414 )));
415 }
416 ensure_unique_ids(&frame.atom_data)?;
417 let prec = self.float_format;
418 let meta_line = if frame.header.spec_version < 2 {
419 frame.header.prebox_header.metadata_line().to_string()
421 } else {
422 self.refresh_metadata_cache(frame)?;
423 self.metadata_cache
424 .as_ref()
425 .expect("metadata_cache populated above")
426 .serialized
427 .clone()
428 };
429 let (boxl, angles) = match frame.header.lattice_vectors() {
430 Some(vecs) => crate::types::lengths_and_angles(vecs),
431 None => (frame.header.boxl, frame.header.angles),
432 };
433 self.scratch.clear();
434 {
435 let buf = &mut self.scratch;
436 let ryu = &mut self.ryu;
437
438 let _ = writeln!(buf, "{}", frame.header.prebox_header.user);
440 let _ = writeln!(buf, "{meta_line}");
441 push_f64(buf, ryu, boxl[0], prec);
442 buf.push(b' ');
443 push_f64(buf, ryu, boxl[1], prec);
444 buf.push(b' ');
445 push_f64(buf, ryu, boxl[2], prec);
446 buf.push(b'\n');
447 push_f64(buf, ryu, angles[0], prec);
448 buf.push(b' ');
449 push_f64(buf, ryu, angles[1], prec);
450 buf.push(b' ');
451 push_f64(buf, ryu, angles[2], prec);
452 buf.push(b'\n');
453 let _ = writeln!(buf, "{}", frame.header.postbox_header[0]);
454 let _ = writeln!(buf, "{}", frame.header.postbox_header[1]);
455 let _ = writeln!(buf, "{}", frame.header.natm_types);
456
457 for (i, n) in frame.header.natms_per_type.iter().enumerate() {
458 if i > 0 {
459 buf.push(b' ');
460 }
461 push_u64(buf, *n as u64);
462 }
463 buf.push(b'\n');
464
465 for (i, m) in frame.header.masses_per_type.iter().enumerate() {
466 if i > 0 {
467 buf.push(b' ');
468 }
469 push_f64(buf, ryu, *m, prec);
470 }
471 buf.push(b'\n');
472
473 let mut atom_idx_offset = 0;
475 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate() {
476 let symbol = &frame.atom_data[atom_idx_offset].symbol;
477 let _ = writeln!(buf, "{symbol}");
478 let _ = writeln!(buf, "Coordinates of Component {}", type_idx + 1);
479
480 for i in 0..num_atoms_in_type {
481 let atom = &frame.atom_data[atom_idx_offset + i];
482 push_xyz_line(
483 buf,
484 ryu,
485 atom.x,
486 atom.y,
487 atom.z,
488 prec,
489 written_fixed_flag(atom.fixed, frame.header.spec_version),
490 atom.atom_id,
491 );
492 }
493 atom_idx_offset += num_atoms_in_type;
494 }
495
496 if frame.has_velocities() {
498 buf.push(b'\n');
499
500 let mut vel_idx_offset = 0;
501 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
502 {
503 let symbol = &frame.atom_data[vel_idx_offset].symbol;
504 let _ = writeln!(buf, "{symbol}");
505 let _ = writeln!(buf, "Velocities of Component {}", type_idx + 1);
506
507 for i in 0..num_atoms_in_type {
508 let atom = &frame.atom_data[vel_idx_offset + i];
509 let [vx, vy, vz] = atom.velocity().unwrap_or([0.0; 3]);
510 push_xyz_line(
511 buf,
512 ryu,
513 vx,
514 vy,
515 vz,
516 prec,
517 written_fixed_flag(atom.fixed, frame.header.spec_version),
518 atom.atom_id,
519 );
520 }
521 vel_idx_offset += num_atoms_in_type;
522 }
523 }
524
525 if frame.has_forces() {
527 buf.push(b'\n');
528
529 let mut force_idx_offset = 0;
530 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
531 {
532 let symbol = &frame.atom_data[force_idx_offset].symbol;
533 let _ = writeln!(buf, "{symbol}");
534 let _ = writeln!(buf, "Forces of Component {}", type_idx + 1);
535
536 for i in 0..num_atoms_in_type {
537 let atom = &frame.atom_data[force_idx_offset + i];
538 let [fx, fy, fz] = atom.force().unwrap_or([0.0; 3]);
539 push_xyz_line(
540 buf,
541 ryu,
542 fx,
543 fy,
544 fz,
545 prec,
546 written_fixed_flag(atom.fixed, frame.header.spec_version),
547 atom.atom_id,
548 );
549 }
550 force_idx_offset += num_atoms_in_type;
551 }
552 }
553
554 if frame.has_energies() {
556 buf.push(b'\n');
557
558 let mut energy_idx_offset = 0;
559 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
560 {
561 let symbol = &frame.atom_data[energy_idx_offset].symbol;
562 let _ = writeln!(buf, "{symbol}");
563 let _ = writeln!(buf, "Energies of Component {}", type_idx + 1);
564
565 for i in 0..num_atoms_in_type {
566 let atom = &frame.atom_data[energy_idx_offset + i];
567 let e = atom.energy().unwrap_or(0.0);
568 push_scalar_line(
569 buf,
570 ryu,
571 e,
572 prec,
573 written_fixed_flag(atom.fixed, frame.header.spec_version),
574 atom.atom_id,
575 );
576 }
577 energy_idx_offset += num_atoms_in_type;
578 }
579 }
580
581 if frame.has_charges() {
582 buf.push(b'\n');
583 let mut off = 0;
584 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
585 {
586 let symbol = &frame.atom_data[off].symbol;
587 let _ = writeln!(buf, "{symbol}");
588 let _ = writeln!(buf, "Charges of Component {}", type_idx + 1);
589 for i in 0..num_atoms_in_type {
590 let atom = &frame.atom_data[off + i];
591 let q = atom.charge().unwrap_or(0.0);
592 push_scalar_line(
593 buf,
594 ryu,
595 q,
596 prec,
597 written_fixed_flag(atom.fixed, frame.header.spec_version),
598 atom.atom_id,
599 );
600 }
601 off += num_atoms_in_type;
602 }
603 }
604
605 if frame.has_spins() {
606 buf.push(b'\n');
607 let mut off = 0;
608 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
609 {
610 let symbol = &frame.atom_data[off].symbol;
611 let _ = writeln!(buf, "{symbol}");
612 let _ = writeln!(buf, "Spins of Component {}", type_idx + 1);
613 for i in 0..num_atoms_in_type {
614 let atom = &frame.atom_data[off + i];
615 let s = atom.spin().unwrap_or(0.0);
616 push_scalar_line(
617 buf,
618 ryu,
619 s,
620 prec,
621 written_fixed_flag(atom.fixed, frame.header.spec_version),
622 atom.atom_id,
623 );
624 }
625 off += num_atoms_in_type;
626 }
627 }
628
629 if frame.has_magmoms() {
630 buf.push(b'\n');
631 let mut off = 0;
632 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
633 {
634 let symbol = &frame.atom_data[off].symbol;
635 let _ = writeln!(buf, "{symbol}");
636 let _ = writeln!(buf, "Magmoms of Component {}", type_idx + 1);
637 for i in 0..num_atoms_in_type {
638 let atom = &frame.atom_data[off + i];
639 let [mx, my, mz] = atom.magmom().unwrap_or([0.0; 3]);
640 push_xyz_line(
641 buf,
642 ryu,
643 mx,
644 my,
645 mz,
646 prec,
647 written_fixed_flag(atom.fixed, frame.header.spec_version),
648 atom.atom_id,
649 );
650 }
651 off += num_atoms_in_type;
652 }
653 }
654
655 if frame.has_displacements() {
656 buf.push(b'\n');
657 let mut off = 0;
658 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
659 {
660 let symbol = &frame.atom_data[off].symbol;
661 let _ = writeln!(buf, "{symbol}");
662 let _ = writeln!(buf, "Displacements of Component {}", type_idx + 1);
663 for i in 0..num_atoms_in_type {
664 let atom = &frame.atom_data[off + i];
665 let [dx, dy, dz] = atom.displacement().unwrap_or([0.0; 3]);
666 push_xyz_line(
667 buf,
668 ryu,
669 dx,
670 dy,
671 dz,
672 prec,
673 written_fixed_flag(atom.fixed, frame.header.spec_version),
674 atom.atom_id,
675 );
676 }
677 off += num_atoms_in_type;
678 }
679 }
680
681 if frame.has_spreads() {
682 buf.push(b'\n');
683 let mut off = 0;
684 for (type_idx, &num_atoms_in_type) in frame.header.natms_per_type.iter().enumerate()
685 {
686 let symbol = &frame.atom_data[off].symbol;
687 let _ = writeln!(buf, "{symbol}");
688 let _ = writeln!(buf, "Spreads of Component {}", type_idx + 1);
689 for i in 0..num_atoms_in_type {
690 let atom = &frame.atom_data[off + i];
691 let [sx, sy, sz] = atom.spread().unwrap_or([0.0; 3]);
692 if !crate::types::spread_row_ok([sx, sy, sz]) {
693 let err = if sx.is_finite() && sy.is_finite() && sz.is_finite() {
694 crate::types::invalid_spread(off + i)
695 } else {
696 crate::error::ParseError::NonFiniteValue(format!(
697 "spreads atom {}",
698 off + i
699 ))
700 };
701 return Err(WriteError::Format(err));
702 }
703 push_xyz_line(
704 buf,
705 ryu,
706 sx,
707 sy,
708 sz,
709 prec,
710 written_fixed_flag(atom.fixed, frame.header.spec_version),
711 atom.atom_id,
712 );
713 }
714 off += num_atoms_in_type;
715 }
716 }
717 }
718
719 self.writer.write_all(&self.scratch)?;
720 Ok(())
721 }
722
723 pub fn flush(&mut self) -> io::Result<()> {
725 self.writer.flush()
726 }
727
728 pub fn extend<'a>(
732 &mut self,
733 frames: impl Iterator<Item = &'a ConFrame>,
734 ) -> Result<(), WriteError> {
735 for frame in frames {
736 self.write_frame(frame)?;
737 }
738 Ok(())
739 }
740}
741
742fn ensure_unique_ids(atoms: &[crate::types::AtomDatum]) -> Result<(), WriteError> {
743 let n = atoms.len();
744 if n == 0 {
745 return Ok(());
746 }
747 if atoms
749 .iter()
750 .enumerate()
751 .all(|(i, atom)| atom.atom_id == i as u64)
752 {
753 return Ok(());
754 }
755 let mut seen_ids = std::collections::HashSet::with_capacity(n);
756 for atom in atoms {
757 if !seen_ids.insert(atom.atom_id) {
758 return Err(WriteError::Format(
759 crate::error::ParseError::ValidationError(format!(
760 "atom id {} is repeated in the frame",
761 atom.atom_id
762 )),
763 ));
764 }
765 }
766 Ok(())
767}
768
769fn written_fixed_flag(fixed: [bool; 3], spec_version: u32) -> u8 {
770 if spec_version < 2 {
771 if fixed.iter().any(|axis| *axis) { 1 } else { 0 }
772 } else {
773 encode_fixed_bitmask(fixed)
774 }
775}
776
777fn push_u64(buf: &mut Vec<u8>, n: u64) {
778 push_u128(buf, u128::from(n));
779}
780
781fn push_u128(buf: &mut Vec<u8>, mut n: u128) {
782 let mut tmp = [0u8; 40];
783 let mut i = 40;
784 if n == 0 {
785 buf.push(b'0');
786 return;
787 }
788 while n > 0 {
789 i -= 1;
790 tmp[i] = b'0' + (n % 10) as u8;
791 n /= 10;
792 }
793 buf.extend_from_slice(&tmp[i..]);
794}
795
796fn push_f64(buf: &mut Vec<u8>, ryu: &mut ryu::Buffer, value: f64, format: FloatFormat) {
797 match format {
798 FloatFormat::DecimalPlaces(precision) => push_f64_prec(buf, value, precision),
799 FloatFormat::RoundTrip => {
800 let text = if value.is_finite() {
803 ryu.format_finite(value)
804 } else {
805 ryu.format(value)
806 };
807 buf.extend_from_slice(text.as_bytes());
808 }
809 }
810}
811
812fn push_f64_prec(buf: &mut Vec<u8>, v: f64, prec: usize) {
815 if !v.is_finite() || prec != 6 {
818 let _ = write!(buf, "{v:.prec$}");
819 return;
820 }
821 if v.is_sign_negative() {
822 buf.push(b'-');
823 }
824 let ax = v.abs();
825 if prec == 0 {
826 push_u128(buf, ax.round() as u128);
827 return;
828 }
829 let scale = 10u128.pow(prec as u32);
830 let n = (ax * scale as f64).round() as u128;
831 let int_part = n / scale;
832 let frac = n % scale;
833 push_u128(buf, int_part);
834 buf.push(b'.');
835 let mut tmp = [b'0'; 20];
836 let mut x = frac;
837 let mut i = prec;
838 while i > 0 {
839 i -= 1;
840 tmp[i] = b'0' + (x % 10) as u8;
841 x /= 10;
842 }
843 buf.extend_from_slice(&tmp[..prec]);
844}
845
846fn push_xyz_line(
847 buf: &mut Vec<u8>,
848 ryu: &mut ryu::Buffer,
849 x: f64,
850 y: f64,
851 z: f64,
852 prec: FloatFormat,
853 fixed: u8,
854 atom_id: u64,
855) {
856 push_f64(buf, ryu, x, prec);
857 buf.push(b' ');
858 push_f64(buf, ryu, y, prec);
859 buf.push(b' ');
860 push_f64(buf, ryu, z, prec);
861 buf.push(b' ');
862 push_u64(buf, u64::from(fixed));
863 buf.push(b' ');
864 push_u64(buf, atom_id);
865 buf.push(b'\n');
866}
867
868fn push_scalar_line(
869 buf: &mut Vec<u8>,
870 ryu: &mut ryu::Buffer,
871 v: f64,
872 prec: FloatFormat,
873 fixed: u8,
874 atom_id: u64,
875) {
876 push_f64(buf, ryu, v, prec);
877 buf.push(b' ');
878 push_u64(buf, u64::from(fixed));
879 buf.push(b' ');
880 push_u64(buf, atom_id);
881 buf.push(b'\n');
882}
883
884pub(crate) fn stamp_legacy_velocity_time(
892 meta: &mut BTreeMap<String, Value>,
893 spec_version: u32,
894 has_velocity: bool,
895) {
896 if !has_velocity {
897 return;
898 }
899 let minor = meta
900 .get(meta::CON_SPEC_MINOR)
901 .and_then(Value::as_u64)
902 .unwrap_or(0);
903 if spec_version >= 3 && minor >= 1 {
904 return;
905 }
906 let mut units = match meta.get(meta::UNITS).cloned() {
911 Some(Value::Object(map)) => Value::Object(map),
912 Some(_) => return,
913 None => crate::units::default_v3_units_json(),
914 };
915 if let Some(object) = units.as_object_mut() {
916 object.insert(
917 "time".to_string(),
918 Value::String(crate::units::NATURAL_TIME_UNIT.to_string()),
919 );
920 meta.insert(meta::UNITS.to_string(), units);
921 if meta.contains_key(meta::CON_SPEC_MINOR) {
923 meta.insert(meta::CON_SPEC_MINOR.to_string(), Value::from(1u64));
924 }
925 }
926}
927
928pub fn binding_rewrite_frame(frame: &ConFrame) -> Result<ConFrame, crate::error::ParseError> {
936 let mut meta = frame.header.metadata.clone();
937 meta.remove(meta::CON_SPEC_VERSION);
938 meta.remove(meta::SECTIONS);
939 let has_velocity = frame.atom_data.iter().any(|atom| atom.velocity().is_some());
940 stamp_legacy_velocity_time(&mut meta, frame.header.spec_version, has_velocity);
941
942 let mut builder = ConFrameBuilder::new(frame.header.boxl, frame.header.angles);
943 builder
944 .prebox_header(frame.header.prebox_header.user.as_str())
945 .postbox_header(frame.header.postbox_header.clone())
946 .metadata(meta);
947
948 let mut masses = Vec::with_capacity(frame.atom_data.len());
949 for (type_index, &count) in frame.header.natms_per_type.iter().enumerate() {
950 let mass = frame
951 .header
952 .masses_per_type
953 .get(type_index)
954 .copied()
955 .unwrap_or(0.0);
956 for _ in 0..count {
957 masses.push(mass);
958 }
959 }
960
961 for (index, atom) in frame.atom_data.iter().enumerate() {
962 let mass = masses.get(index).copied().unwrap_or(0.0);
963 builder.add_atom(
964 atom.symbol.as_ref(),
965 atom.x,
966 atom.y,
967 atom.z,
968 atom.fixed,
969 atom.atom_id,
970 mass,
971 );
972 if let Some(velocity) = atom.velocity() {
973 builder.with_velocity(velocity);
974 }
975 if let Some(force) = atom.force() {
976 builder.with_force(force);
977 }
978 if let Some(energy) = atom.energy() {
979 builder.with_energy(energy);
980 }
981 if let Some(charge) = atom.charge() {
982 builder.with_charge(charge);
983 }
984 if let Some(spin) = atom.spin() {
985 builder.with_spin(spin);
986 }
987 if let Some(magmom) = atom.magmom() {
988 builder.with_magmom(magmom);
989 }
990 if let Some(displacement) = atom.displacement() {
991 builder.with_displacement(displacement);
992 }
993 if let Some(spread) = atom.spread() {
994 builder.with_spread(spread);
995 }
996 }
997 builder.build()
998}
999
1000impl ConFrameWriter<File> {
1002 pub fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
1006 let file = File::create(path)?;
1007 Ok(Self::new(file))
1008 }
1009
1010 pub fn from_path_with_precision<P: AsRef<Path>>(path: P, precision: usize) -> io::Result<Self> {
1012 let file = File::create(path)?;
1013 Ok(Self::with_precision(file, precision))
1014 }
1015}
1016
1017impl ConFrameWriter<flate2::write::GzEncoder<File>> {
1019 pub fn from_path_gzip<P: AsRef<Path>>(path: P) -> io::Result<Self> {
1021 let encoder = crate::compression::gzip_writer(path.as_ref())?;
1022 Ok(Self::new(encoder))
1023 }
1024
1025 pub fn from_path_gzip_with_precision<P: AsRef<Path>>(
1027 path: P,
1028 precision: usize,
1029 ) -> io::Result<Self> {
1030 let encoder = crate::compression::gzip_writer(path.as_ref())?;
1031 Ok(Self::with_precision(encoder, precision))
1032 }
1033}
1034
1035#[cfg(feature = "zstd")]
1038impl ConFrameWriter<zstd::stream::write::AutoFinishEncoder<'static, File>> {
1039 pub fn from_path_zstd<P: AsRef<Path>>(path: P) -> io::Result<Self> {
1041 let encoder = crate::compression::zstd_writer(path.as_ref())?;
1042 Ok(Self::new(encoder))
1043 }
1044
1045 pub fn from_path_zstd_with_precision<P: AsRef<Path>>(
1047 path: P,
1048 precision: usize,
1049 ) -> io::Result<Self> {
1050 let encoder = crate::compression::zstd_writer(path.as_ref())?;
1051 Ok(Self::with_precision(encoder, precision))
1052 }
1053}
1054
1055#[cfg(test)]
1056mod float_format_tests {
1057 use super::push_f64_prec;
1058
1059 fn formatted(v: f64, prec: usize) -> String {
1060 let mut buf = Vec::new();
1061 push_f64_prec(&mut buf, v, prec);
1062 String::from_utf8(buf).expect("utf8")
1063 }
1064
1065 #[test]
1066 fn matches_std_fixed_precision() {
1067 let vals = [
1068 0.0,
1069 -0.0,
1070 1.0,
1071 -1.0,
1072 0.9045,
1073 6.975_299_999_999_995,
1074 63.546,
1075 1.008,
1076 15.3456,
1077 90.0,
1078 218.0,
1079 1e-6,
1080 -1.23456789,
1081 10.0,
1082 ];
1083 for prec in [0usize, 6] {
1084 for v in vals {
1085 let got = formatted(v, prec);
1086 let exp = format!("{v:.prec$}");
1087 assert_eq!(got, exp, "v={v:?} prec={prec}");
1088 }
1089 }
1090 }
1091}
1092
1093#[cfg(test)]
1094mod section_arity_tests {
1095 use super::require_minor2_section_arity;
1096 use serde_json::json;
1097
1098 #[test]
1099 fn minor2_lists_a_new_section_in_section_arity() {
1100 let widgets = vec![json!("widgets")];
1101 let err = require_minor2_section_arity(2, &widgets, &serde_json::Map::new()).unwrap_err();
1102 let msg = err.to_string();
1103 assert!(msg.contains("section_arity"), "{msg}");
1104
1105 let mut listed = serde_json::Map::new();
1106 listed.insert("section_arity".into(), json!({"widgets": 3}));
1107 require_minor2_section_arity(2, &widgets, &listed).unwrap();
1108
1109 let reserved = vec![json!("velocities"), json!("energies")];
1110 require_minor2_section_arity(2, &reserved, &serde_json::Map::new()).unwrap();
1111 require_minor2_section_arity(1, &widgets, &serde_json::Map::new()).unwrap();
1112 }
1113}