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