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