1#![allow(clippy::needless_range_loop, clippy::should_implement_trait)]
6use super::functions::*;
7use crate::{Error, Result};
8use oxiphysics_core::math::Vec3;
9use std::io::{BufRead, BufReader, Read, Write};
10
11#[allow(dead_code)]
13#[derive(Debug, Clone)]
14pub struct DumpColumnSpec {
15 pub columns: Vec<String>,
17}
18impl DumpColumnSpec {
19 pub fn standard() -> Self {
21 Self {
22 columns: vec![
23 "id".into(),
24 "type".into(),
25 "x".into(),
26 "y".into(),
27 "z".into(),
28 "vx".into(),
29 "vy".into(),
30 "vz".into(),
31 ],
32 }
33 }
34 pub fn positions_only() -> Self {
36 Self {
37 columns: vec![
38 "id".into(),
39 "type".into(),
40 "x".into(),
41 "y".into(),
42 "z".into(),
43 ],
44 }
45 }
46 pub fn header_line(&self) -> String {
48 format!("ITEM: ATOMS {}", self.columns.join(" "))
49 }
50 pub fn len(&self) -> usize {
52 self.columns.len()
53 }
54 pub fn is_empty(&self) -> bool {
56 self.columns.is_empty()
57 }
58 pub fn contains(&self, name: &str) -> bool {
60 self.columns.iter().any(|c| c == name)
61 }
62 pub fn index_of(&self, name: &str) -> Option<usize> {
64 self.columns.iter().position(|c| c == name)
65 }
66}
67pub struct LammpsRestartWriter;
70impl LammpsRestartWriter {
71 pub fn write<W: std::io::Write>(
84 writer: &mut W,
85 timestep: i64,
86 masses: &[f64],
87 atoms: &[LammpsAtom],
88 ) -> Result<()> {
89 writer.write_all(b"LRST")?;
90 writer.write_all(&1_i32.to_le_bytes())?;
91 writer.write_all(×tep.to_le_bytes())?;
92 writer.write_all(&(atoms.len() as i64).to_le_bytes())?;
93 writer.write_all(&(masses.len() as i32).to_le_bytes())?;
94 for &m in masses {
95 writer.write_all(&m.to_le_bytes())?;
96 }
97 for a in atoms {
98 writer.write_all(&(a.id as i32).to_le_bytes())?;
99 writer.write_all(&(a.type_id as i32).to_le_bytes())?;
100 writer.write_all(&a.position.x.to_le_bytes())?;
101 writer.write_all(&a.position.y.to_le_bytes())?;
102 writer.write_all(&a.position.z.to_le_bytes())?;
103 writer.write_all(&a.velocity.x.to_le_bytes())?;
104 writer.write_all(&a.velocity.y.to_le_bytes())?;
105 writer.write_all(&a.velocity.z.to_le_bytes())?;
106 }
107 Ok(())
108 }
109}
110#[allow(dead_code)]
112#[derive(Debug, Clone)]
113pub struct LammpsBox {
114 pub lo: [f64; 3],
116 pub hi: [f64; 3],
118 pub tilt: [f64; 3],
120}
121#[allow(dead_code)]
122impl LammpsBox {
123 pub fn orthogonal(lo: [f64; 3], hi: [f64; 3]) -> Self {
125 Self {
126 lo,
127 hi,
128 tilt: [0.0; 3],
129 }
130 }
131 pub fn lengths(&self) -> [f64; 3] {
133 [
134 self.hi[0] - self.lo[0],
135 self.hi[1] - self.lo[1],
136 self.hi[2] - self.lo[2],
137 ]
138 }
139 pub fn volume(&self) -> f64 {
141 let l = self.lengths();
142 l[0] * l[1] * l[2]
143 }
144 pub fn number_density(&self, n_atoms: usize) -> f64 {
146 let vol = self.volume();
147 if vol < 1e-30 {
148 return 0.0;
149 }
150 n_atoms as f64 / vol
151 }
152 pub fn to_region_command(&self, region_id: &str) -> String {
154 format!(
155 "region {} block {} {} {} {} {} {}\n",
156 region_id, self.lo[0], self.hi[0], self.lo[1], self.hi[1], self.lo[2], self.hi[2],
157 )
158 }
159 pub fn contains(&self, pos: [f64; 3]) -> bool {
161 (0..3).all(|d| pos[d] >= self.lo[d] && pos[d] < self.hi[d])
162 }
163 pub fn wrap_pbc(&self, pos: [f64; 3]) -> [f64; 3] {
165 let mut out = pos;
166 for d in 0..3 {
167 let l = self.hi[d] - self.lo[d];
168 if l > 1e-30 {
169 out[d] = out[d] - (((out[d] - self.lo[d]) / l).floor()) * l + self.lo[d];
170 }
171 }
172 out
173 }
174}
175#[allow(dead_code)]
177pub struct LammpsInputScript;
178#[allow(dead_code)]
179impl LammpsInputScript {
180 pub fn minimize_script(force_tol: f64, energy_tol: f64) -> String {
185 format!("# Energy minimisation\nminimize {energy_tol} {force_tol} 1000 10000\n")
186 }
187 pub fn npt_script(temp: f64, press: f64, t_damp: f64, p_damp: f64, n_steps: u64) -> String {
195 format!(
196 "# NPT ensemble\nfix 1 all npt temp {temp} {temp} {t_damp} iso {press} {press} {p_damp}\nrun {n_steps}\nunfix 1\n"
197 )
198 }
199}
200#[allow(dead_code)]
201impl LammpsInputScript {
202 pub fn nve_script(n_steps: u64, dt: f64) -> String {
204 let mut s = String::new();
205 s.push_str(&format!("timestep {dt}\n"));
206 s.push_str("fix 1 all nve\n");
207 s.push_str(&format!("run {n_steps}\n"));
208 s.push_str("unfix 1\n");
209 s
210 }
211 pub fn thermalise_script(temp: f64, damp: f64, n_steps: u64, seed: u64) -> String {
213 let mut s = String::new();
214 s.push_str(&format!(
215 "fix therm all langevin {temp} {temp} {damp} {seed}\n"
216 ));
217 s.push_str("fix nve_int all nve\n");
218 s.push_str(&format!("run {n_steps}\n"));
219 s.push_str("unfix therm\n");
220 s.push_str("unfix nve_int\n");
221 s
222 }
223 pub fn dump_command(
225 dump_id: &str,
226 group: &str,
227 style: &str,
228 every: u64,
229 filename: &str,
230 fields: &str,
231 ) -> String {
232 format!("dump {dump_id} {group} {style} {every} {filename} {fields}\n")
233 }
234 pub fn thermo_output(every: u64, keywords: &[&str]) -> String {
236 let mut s = format!("thermo {every}\n");
237 if !keywords.is_empty() {
238 s.push_str(&format!("thermo_style custom {}\n", keywords.join(" ")));
239 }
240 s
241 }
242 pub fn units(style: &str) -> String {
244 format!("units {style}\n")
245 }
246 pub fn boundary(style: &str) -> String {
248 format!("boundary {style}\n")
249 }
250 pub fn read_data(filename: &str) -> String {
252 format!("read_data {filename}\n")
253 }
254}
255pub struct LammpsBinaryDumpWriter;
260impl LammpsBinaryDumpWriter {
261 pub fn write_frame<W: std::io::Write>(
274 writer: &mut W,
275 timestep: i64,
276 box_bounds: [[f64; 2]; 3],
277 atoms: &[LammpsAtom],
278 ) -> Result<()> {
279 writer.write_all(×tep.to_le_bytes())?;
280 writer.write_all(&(atoms.len() as i64).to_le_bytes())?;
281 for b in &box_bounds {
282 writer.write_all(&b[0].to_le_bytes())?;
283 writer.write_all(&b[1].to_le_bytes())?;
284 }
285 for a in atoms {
286 writer.write_all(&(a.id as i32).to_le_bytes())?;
287 writer.write_all(&(a.type_id as i32).to_le_bytes())?;
288 writer.write_all(&a.position.x.to_le_bytes())?;
289 writer.write_all(&a.position.y.to_le_bytes())?;
290 writer.write_all(&a.position.z.to_le_bytes())?;
291 writer.write_all(&a.velocity.x.to_le_bytes())?;
292 writer.write_all(&a.velocity.y.to_le_bytes())?;
293 writer.write_all(&a.velocity.z.to_le_bytes())?;
294 }
295 Ok(())
296 }
297 pub fn frame_byte_size(n_atoms: usize) -> usize {
299 8 + 8 + 6 * 8 + n_atoms * (4 + 4 + 6 * 8)
300 }
301}
302#[allow(dead_code)]
304pub struct LammpsDataWriter;
305#[allow(dead_code)]
306impl LammpsDataWriter {
307 pub fn write_header(
311 n_atoms: usize,
312 n_bonds: usize,
313 box_lo: [f64; 3],
314 box_hi: [f64; 3],
315 ) -> String {
316 let mut s = String::new();
317 s.push_str("LAMMPS data file\n\n");
318 s.push_str(&format!("{} atoms\n", n_atoms));
319 if n_bonds > 0 {
320 s.push_str(&format!("{} bonds\n", n_bonds));
321 }
322 s.push('\n');
323 s.push_str(&format!("{} {} xlo xhi\n", box_lo[0], box_hi[0]));
324 s.push_str(&format!("{} {} ylo yhi\n", box_lo[1], box_hi[1]));
325 s.push_str(&format!("{} {} zlo zhi\n", box_lo[2], box_hi[2]));
326 s
327 }
328 pub fn write_atoms_atomic(positions: &[[f64; 3]], masses: &[f64], types: &[u32]) -> String {
332 let _ = masses;
333 let mut s = String::from("\nAtoms # atomic\n\n");
334 for (i, (pos, &t)) in positions.iter().zip(types.iter()).enumerate() {
335 s.push_str(&format!(
336 "{} {} {} {} {}\n",
337 i + 1,
338 t,
339 pos[0],
340 pos[1],
341 pos[2]
342 ));
343 }
344 s
345 }
346 pub fn write_atoms_charge(
348 positions: &[[f64; 3]],
349 masses: &[f64],
350 charges: &[f64],
351 types: &[u32],
352 ) -> String {
353 let _ = masses;
354 let mut s = String::from("\nAtoms # charge\n\n");
355 for (i, ((pos, &q), &t)) in positions
356 .iter()
357 .zip(charges.iter())
358 .zip(types.iter())
359 .enumerate()
360 {
361 s.push_str(&format!(
362 "{} {} {} {} {} {}\n",
363 i + 1,
364 t,
365 q,
366 pos[0],
367 pos[1],
368 pos[2]
369 ));
370 }
371 s
372 }
373 pub fn write_bonds(bonds: &[(u32, u32, u32)]) -> String {
377 let mut s = String::from("\nBonds\n\n");
378 for (idx, &(btype, i, j)) in bonds.iter().enumerate() {
379 s.push_str(&format!("{} {} {} {}\n", idx + 1, btype, i, j));
380 }
381 s
382 }
383 pub fn write_complete(
385 positions: &[[f64; 3]],
386 masses: &[f64],
387 types: &[u32],
388 box_lo: [f64; 3],
389 box_hi: [f64; 3],
390 ) -> String {
391 let mut s = Self::write_header(positions.len(), 0, box_lo, box_hi);
392 let max_type = *types.iter().max().unwrap_or(&1) as usize;
393 s.push_str(&format!("{} atom types\n", max_type));
394 s.push_str("\nMasses\n\n");
395 let mut type_mass = vec![1.0_f64; max_type + 1];
396 for (&t, &m) in types.iter().zip(masses.iter()) {
397 type_mass[t as usize] = m;
398 }
399 for ti in 1..=max_type {
400 s.push_str(&format!("{} {}\n", ti, type_mass[ti]));
401 }
402 s.push_str(&Self::write_atoms_atomic(positions, masses, types));
403 s
404 }
405}
406#[allow(dead_code)]
407impl LammpsDataWriter {
408 pub fn write_atoms_full(
412 positions: &[[f64; 3]],
413 charges: &[f64],
414 types: &[u32],
415 mol_ids: &[u32],
416 ) -> String {
417 let mut s = String::from("\nAtoms # full\n\n");
418 for (i, (((&pos, &q), &t), &mol)) in positions
419 .iter()
420 .zip(charges.iter())
421 .zip(types.iter())
422 .zip(mol_ids.iter())
423 .enumerate()
424 {
425 s.push_str(&format!(
426 "{} {} {} {} {} {} {}\n",
427 i + 1,
428 mol,
429 t,
430 q,
431 pos[0],
432 pos[1],
433 pos[2]
434 ));
435 }
436 s
437 }
438 pub fn write_atoms_molecular(positions: &[[f64; 3]], types: &[u32], mol_ids: &[u32]) -> String {
442 let mut s = String::from("\nAtoms # molecular\n\n");
443 for (i, ((&pos, &t), &mol)) in positions
444 .iter()
445 .zip(types.iter())
446 .zip(mol_ids.iter())
447 .enumerate()
448 {
449 s.push_str(&format!(
450 "{} {} {} {} {} {}\n",
451 i + 1,
452 mol,
453 t,
454 pos[0],
455 pos[1],
456 pos[2]
457 ));
458 }
459 s
460 }
461}
462#[allow(dead_code)]
464pub struct LammpsDataReader {
465 pub(super) positions: Vec<[f64; 3]>,
466 pub(super) masses: Vec<f64>,
467 pub(super) types: Vec<u32>,
468 pub(super) box_lo: [f64; 3],
469 pub(super) box_hi: [f64; 3],
470}
471#[allow(dead_code)]
472impl LammpsDataReader {
473 pub fn from_str(data: &str) -> Result<Self> {
475 let mut positions: Vec<[f64; 3]> = Vec::new();
476 let mut masses_map: Vec<(usize, f64)> = Vec::new();
477 let mut types: Vec<u32> = Vec::new();
478 let mut box_lo = [0.0_f64; 3];
479 let mut box_hi = [10.0_f64; 3];
480 let mut in_atoms = false;
481 let mut in_masses = false;
482 for raw in data.lines() {
483 let line = raw.trim();
484 if line.is_empty() || line.starts_with('#') {
485 continue;
486 }
487 if line.starts_with("Atoms") {
488 in_atoms = true;
489 in_masses = false;
490 continue;
491 }
492 if line.starts_with("Masses") {
493 in_masses = true;
494 in_atoms = false;
495 continue;
496 }
497 if line.starts_with("Bonds") || line.starts_with("Velocities") {
498 in_atoms = false;
499 in_masses = false;
500 continue;
501 }
502 if line.contains("xlo xhi") {
503 let p = line.split_whitespace().collect::<Vec<_>>();
504 box_lo[0] = p[0]
505 .parse::<f64>()
506 .map_err(|e| Error::Parse(e.to_string()))?;
507 box_hi[0] = p[1]
508 .parse::<f64>()
509 .map_err(|e| Error::Parse(e.to_string()))?;
510 continue;
511 }
512 if line.contains("ylo yhi") {
513 let p = line.split_whitespace().collect::<Vec<_>>();
514 box_lo[1] = p[0]
515 .parse::<f64>()
516 .map_err(|e| Error::Parse(e.to_string()))?;
517 box_hi[1] = p[1]
518 .parse::<f64>()
519 .map_err(|e| Error::Parse(e.to_string()))?;
520 continue;
521 }
522 if line.contains("zlo zhi") {
523 let p = line.split_whitespace().collect::<Vec<_>>();
524 box_lo[2] = p[0]
525 .parse::<f64>()
526 .map_err(|e| Error::Parse(e.to_string()))?;
527 box_hi[2] = p[1]
528 .parse::<f64>()
529 .map_err(|e| Error::Parse(e.to_string()))?;
530 continue;
531 }
532 if in_masses {
533 let p = line.split_whitespace().collect::<Vec<_>>();
534 if p.len() >= 2 {
535 let tid = p[0]
536 .parse::<usize>()
537 .map_err(|e| Error::Parse(e.to_string()))?;
538 let m = p[1]
539 .parse::<f64>()
540 .map_err(|e| Error::Parse(e.to_string()))?;
541 masses_map.push((tid, m));
542 }
543 continue;
544 }
545 if in_atoms {
546 let p = line.split_whitespace().collect::<Vec<_>>();
547 if p.len() >= 5 {
548 let t = p[1]
549 .parse::<u32>()
550 .map_err(|e| Error::Parse(e.to_string()))?;
551 let x = p[2]
552 .parse::<f64>()
553 .map_err(|e| Error::Parse(e.to_string()))?;
554 let y = p[3]
555 .parse::<f64>()
556 .map_err(|e| Error::Parse(e.to_string()))?;
557 let z = p[4]
558 .parse::<f64>()
559 .map_err(|e| Error::Parse(e.to_string()))?;
560 positions.push([x, y, z]);
561 types.push(t);
562 }
563 continue;
564 }
565 }
566 let max_type = types.iter().copied().max().unwrap_or(1) as usize;
567 let mut type_mass = vec![1.0_f64; max_type + 1];
568 for (tid, m) in masses_map {
569 if tid <= max_type {
570 type_mass[tid] = m;
571 }
572 }
573 let masses: Vec<f64> = types.iter().map(|&t| type_mass[t as usize]).collect();
574 Ok(Self {
575 positions,
576 masses,
577 types,
578 box_lo,
579 box_hi,
580 })
581 }
582 pub fn positions(&self) -> &[[f64; 3]] {
584 &self.positions
585 }
586 pub fn masses(&self) -> &[f64] {
588 &self.masses
589 }
590 pub fn types(&self) -> &[u32] {
592 &self.types
593 }
594 pub fn box_bounds(&self) -> ([f64; 3], [f64; 3]) {
596 (self.box_lo, self.box_hi)
597 }
598}
599#[derive(Debug, Clone)]
601pub struct LammpsAtom {
602 pub id: usize,
604 pub type_id: usize,
606 pub position: Vec3,
608 pub velocity: Vec3,
610}
611#[allow(dead_code)]
613pub struct LammpsFix;
614#[allow(dead_code)]
615impl LammpsFix {
616 pub fn nve(fix_id: &str, group: &str) -> String {
618 format!("fix {fix_id} {group} nve\n")
619 }
620 pub fn nvt(fix_id: &str, group: &str, t_start: f64, t_end: f64, t_damp: f64) -> String {
622 format!("fix {fix_id} {group} nvt temp {t_start} {t_end} {t_damp}\n")
623 }
624 pub fn langevin(
626 fix_id: &str,
627 group: &str,
628 t_start: f64,
629 t_end: f64,
630 damp: f64,
631 seed: u64,
632 ) -> String {
633 format!("fix {fix_id} {group} langevin {t_start} {t_end} {damp} {seed}\n")
634 }
635 pub fn wall_reflect(fix_id: &str, group: &str, face: &str, coord: f64) -> String {
637 format!("fix {fix_id} {group} wall/reflect {face} EDGE\n")
638 .replace("EDGE", &format!("{coord}"))
639 }
640 pub fn setforce(fix_id: &str, group: &str, fx: f64, fy: f64, fz: f64) -> String {
642 format!("fix {fix_id} {group} setforce {fx} {fy} {fz}\n")
643 }
644 pub fn rigid(fix_id: &str, group: &str, style: &str) -> String {
646 format!("fix {fix_id} {group} rigid {style}\n")
647 }
648}
649#[allow(dead_code)]
651#[derive(Debug, Clone, PartialEq)]
652pub struct LammpsLJPair {
653 pub type_i: u32,
655 pub type_j: u32,
657 pub epsilon: f64,
659 pub sigma: f64,
661 pub cutoff: Option<f64>,
663}
664#[allow(dead_code)]
665impl LammpsLJPair {
666 pub fn potential_energy(&self, r: f64) -> f64 {
670 let sr = self.sigma / r;
671 let sr6 = sr * sr * sr * sr * sr * sr;
672 let sr12 = sr6 * sr6;
673 4.0 * self.epsilon * (sr12 - sr6)
674 }
675 pub fn force_magnitude(&self, r: f64) -> f64 {
679 let sr = self.sigma / r;
680 let sr6 = sr * sr * sr * sr * sr * sr;
681 let sr12 = sr6 * sr6;
682 24.0 * self.epsilon / r * (2.0 * sr12 - sr6)
683 }
684 pub fn r_min(&self) -> f64 {
686 2.0_f64.powf(1.0 / 6.0) * self.sigma
687 }
688 pub fn to_pair_coeff_line(&self) -> String {
690 if let Some(rc) = self.cutoff {
691 format!(
692 "pair_coeff {} {} {} {} {}\n",
693 self.type_i, self.type_j, self.epsilon, self.sigma, rc
694 )
695 } else {
696 format!(
697 "pair_coeff {} {} {} {}\n",
698 self.type_i, self.type_j, self.epsilon, self.sigma
699 )
700 }
701 }
702}
703#[allow(dead_code)]
705#[derive(Debug, Clone)]
706pub struct LammpsThermoRecord {
707 pub step: u64,
709 pub temp: Option<f64>,
711 pub etotal: Option<f64>,
713 pub pe: Option<f64>,
715 pub ke: Option<f64>,
717 pub press: Option<f64>,
719 pub vol: Option<f64>,
721}
722#[allow(dead_code)]
726pub struct LammpsDataSectionBuilder {
727 pub masses: Vec<(usize, f64)>,
729 pub pair_coeffs: Vec<(usize, usize, f64, f64)>,
731 pub bond_coeffs: Vec<(usize, f64, f64)>,
733 pub angle_coeffs: Vec<(usize, f64, f64)>,
735}
736impl LammpsDataSectionBuilder {
737 pub fn new() -> Self {
739 Self {
740 masses: Vec::new(),
741 pair_coeffs: Vec::new(),
742 bond_coeffs: Vec::new(),
743 angle_coeffs: Vec::new(),
744 }
745 }
746 pub fn add_mass(&mut self, type_id: usize, mass: f64) {
748 self.masses.push((type_id, mass));
749 }
750 pub fn add_pair_coeff(&mut self, type_i: usize, type_j: usize, epsilon: f64, sigma: f64) {
752 self.pair_coeffs.push((type_i, type_j, epsilon, sigma));
753 }
754 pub fn add_bond_coeff(&mut self, bond_type: usize, k: f64, r0: f64) {
756 self.bond_coeffs.push((bond_type, k, r0));
757 }
758 pub fn add_angle_coeff(&mut self, angle_type: usize, k: f64, theta0_deg: f64) {
760 self.angle_coeffs.push((angle_type, k, theta0_deg));
761 }
762 pub fn masses_section(&self) -> String {
764 let mut s = "Masses\n\n".to_string();
765 for &(id, m) in &self.masses {
766 s.push_str(&format!("{} {:.6}\n", id, m));
767 }
768 s
769 }
770 pub fn pair_coeffs_section(&self) -> String {
772 let mut s = "Pair Coeffs\n\n".to_string();
773 for &(i, j, eps, sig) in &self.pair_coeffs {
774 s.push_str(&format!("{} {} {:.6} {:.6}\n", i, j, eps, sig));
775 }
776 s
777 }
778 pub fn bond_coeffs_section(&self) -> String {
780 let mut s = "Bond Coeffs\n\n".to_string();
781 for &(bt, k, r0) in &self.bond_coeffs {
782 s.push_str(&format!("{} {:.6} {:.6}\n", bt, k, r0));
783 }
784 s
785 }
786 pub fn angle_coeffs_section(&self) -> String {
788 let mut s = "Angle Coeffs\n\n".to_string();
789 for &(at, k, th) in &self.angle_coeffs {
790 s.push_str(&format!("{} {:.6} {:.6}\n", at, k, th));
791 }
792 s
793 }
794 pub fn build(&self) -> String {
796 let mut out = String::new();
797 if !self.masses.is_empty() {
798 out.push_str(&self.masses_section());
799 out.push('\n');
800 }
801 if !self.pair_coeffs.is_empty() {
802 out.push_str(&self.pair_coeffs_section());
803 out.push('\n');
804 }
805 if !self.bond_coeffs.is_empty() {
806 out.push_str(&self.bond_coeffs_section());
807 out.push('\n');
808 }
809 if !self.angle_coeffs.is_empty() {
810 out.push_str(&self.angle_coeffs_section());
811 out.push('\n');
812 }
813 out
814 }
815}
816#[allow(dead_code)]
820#[derive(Debug, Clone, PartialEq)]
821pub struct LammpsHarmonicBond {
822 pub bond_type: u32,
824 pub k: f64,
826 pub r0: f64,
828}
829#[allow(dead_code)]
830impl LammpsHarmonicBond {
831 pub fn potential_energy(&self, r: f64) -> f64 {
833 self.k * (r - self.r0) * (r - self.r0)
834 }
835 pub fn force_magnitude(&self, r: f64) -> f64 {
837 2.0 * self.k * (r - self.r0)
838 }
839 pub fn to_bond_coeff_line(&self) -> String {
841 format!("bond_coeff {} {} {}\n", self.bond_type, self.k, self.r0)
842 }
843}
844#[allow(dead_code)]
846pub struct LammpsRunBlock {
847 pub timestep: f64,
849 pub run_steps: u64,
851 pub thermo_freq: u64,
853 pub dump_freq: u64,
855 pub ensemble: String,
857 pub temp_target: f64,
859 pub pressure_target: Option<f64>,
861}
862impl LammpsRunBlock {
863 pub fn nve(timestep: f64, run_steps: u64) -> Self {
865 Self {
866 timestep,
867 run_steps,
868 thermo_freq: 100,
869 dump_freq: 1000,
870 ensemble: "nve".to_string(),
871 temp_target: 300.0,
872 pressure_target: None,
873 }
874 }
875 pub fn nvt(timestep: f64, run_steps: u64, temp: f64) -> Self {
877 Self {
878 timestep,
879 run_steps,
880 thermo_freq: 100,
881 dump_freq: 1000,
882 ensemble: "nvt".to_string(),
883 temp_target: temp,
884 pressure_target: None,
885 }
886 }
887 pub fn npt(timestep: f64, run_steps: u64, temp: f64, pressure: f64) -> Self {
889 Self {
890 timestep,
891 run_steps,
892 thermo_freq: 100,
893 dump_freq: 1000,
894 ensemble: "npt".to_string(),
895 temp_target: temp,
896 pressure_target: Some(pressure),
897 }
898 }
899 pub fn thermo_every(mut self, freq: u64) -> Self {
901 self.thermo_freq = freq;
902 self
903 }
904 pub fn dump_every(mut self, freq: u64) -> Self {
906 self.dump_freq = freq;
907 self
908 }
909 pub fn total_time_fs(&self) -> f64 {
911 self.timestep * self.run_steps as f64
912 }
913 pub fn to_script(&self) -> String {
915 let mut s = String::new();
916 s.push_str(&format!("timestep {:.6}\n", self.timestep));
917 s.push_str(&format!("thermo {}\n", self.thermo_freq));
918 s.push_str("thermo_style custom step temp pe ke etotal press vol\n");
919 s.push_str(&format!(
920 "dump 1 all custom {} dump.lammpstrj id type x y z vx vy vz\n",
921 self.dump_freq
922 ));
923 match self.ensemble.as_str() {
924 "nve" => {
925 s.push_str("fix 1 all nve\n");
926 }
927 "nvt" => {
928 s.push_str(&format!(
929 "fix 1 all nvt temp {t:.1} {t:.1} $(100.0*dt)\n",
930 t = self.temp_target
931 ));
932 }
933 "npt" => {
934 let p = self.pressure_target.unwrap_or(1.0);
935 s.push_str(&format!(
936 "fix 1 all npt temp {t:.1} {t:.1} $(100.0*dt) iso {p:.1} {p:.1} $(1000.0*dt)\n",
937 t = self.temp_target,
938 p = p
939 ));
940 }
941 _ => {}
942 }
943 s.push_str(&format!("run {}\n", self.run_steps));
944 s
945 }
946}
947pub struct LammpsDumpReader;
949impl LammpsDumpReader {
950 pub fn read_frame<R: Read>(reader: R) -> Result<(u64, Vec<LammpsAtom>)> {
954 let buf = BufReader::new(reader);
955 let mut lines = buf.lines();
956 let mut timestep: u64 = 0;
957 let mut n_atoms: usize = 0;
958 let mut atoms: Vec<LammpsAtom> = Vec::new();
959 let mut reading_atoms = false;
960 let mut atom_count = 0;
961 while let Some(line) = lines.next() {
962 let line = line?;
963 let trimmed = line.trim();
964 if trimmed == "ITEM: TIMESTEP" {
965 if let Some(ts_line) = lines.next() {
966 timestep = ts_line?
967 .trim()
968 .parse::<u64>()
969 .map_err(|e| Error::Parse(e.to_string()))?;
970 }
971 } else if trimmed == "ITEM: NUMBER OF ATOMS" {
972 if let Some(n_line) = lines.next() {
973 n_atoms = n_line?
974 .trim()
975 .parse::<usize>()
976 .map_err(|e| Error::Parse(e.to_string()))?;
977 atoms.reserve(n_atoms);
978 }
979 } else if trimmed.starts_with("ITEM: BOX BOUNDS") {
980 for _ in 0..3 {
981 lines.next();
982 }
983 } else if trimmed.starts_with("ITEM: ATOMS") {
984 reading_atoms = true;
985 } else if reading_atoms && atom_count < n_atoms {
986 let parts: Vec<&str> = trimmed.split_whitespace().collect();
987 if parts.len() >= 8 {
988 let id = parts[0]
989 .parse::<usize>()
990 .map_err(|e| Error::Parse(e.to_string()))?;
991 let type_id = parts[1]
992 .parse::<usize>()
993 .map_err(|e| Error::Parse(e.to_string()))?;
994 let x = parts[2]
995 .parse::<f64>()
996 .map_err(|e| Error::Parse(e.to_string()))?;
997 let y = parts[3]
998 .parse::<f64>()
999 .map_err(|e| Error::Parse(e.to_string()))?;
1000 let z = parts[4]
1001 .parse::<f64>()
1002 .map_err(|e| Error::Parse(e.to_string()))?;
1003 let vx = parts[5]
1004 .parse::<f64>()
1005 .map_err(|e| Error::Parse(e.to_string()))?;
1006 let vy = parts[6]
1007 .parse::<f64>()
1008 .map_err(|e| Error::Parse(e.to_string()))?;
1009 let vz = parts[7]
1010 .parse::<f64>()
1011 .map_err(|e| Error::Parse(e.to_string()))?;
1012 atoms.push(LammpsAtom {
1013 id,
1014 type_id,
1015 position: Vec3::new(x, y, z),
1016 velocity: Vec3::new(vx, vy, vz),
1017 });
1018 atom_count += 1;
1019 }
1020 }
1021 }
1022 Ok((timestep, atoms))
1023 }
1024}
1025#[allow(dead_code)]
1027pub struct LammpsPairStyle;
1028#[allow(dead_code)]
1029impl LammpsPairStyle {
1030 pub fn lj_cut(cutoff: f64) -> String {
1032 format!("pair_style lj/cut {cutoff}\n")
1033 }
1034 pub fn pair_coeff_lj(type_i: u32, type_j: u32, epsilon: f64, sigma: f64) -> String {
1036 format!("pair_coeff {type_i} {type_j} {epsilon} {sigma}\n")
1037 }
1038 pub fn lj_cut_coul_long(lj_cutoff: f64, coul_cutoff: f64) -> String {
1040 format!("pair_style lj/cut/coul/long {lj_cutoff} {coul_cutoff}\n")
1041 }
1042 pub fn hybrid(styles: &[&str]) -> String {
1044 format!("pair_style hybrid {}\n", styles.join(" "))
1045 }
1046 pub fn pair_modify(options: &str) -> String {
1048 format!("pair_modify {options}\n")
1049 }
1050}
1051#[allow(dead_code)]
1053#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1054pub enum LammpsAtomStyle {
1055 Atomic,
1057 Full,
1059 Molecular,
1061 Charge,
1063}
1064#[allow(dead_code)]
1069pub struct LammpsLogParser;
1070#[allow(dead_code)]
1071impl LammpsLogParser {
1072 pub fn parse_thermo(log_text: &str) -> Vec<LammpsThermoRecord> {
1077 let mut records = Vec::new();
1078 let mut headers: Vec<String> = Vec::new();
1079 let mut in_thermo = false;
1080 for line in log_text.lines() {
1081 let line = line.trim();
1082 if line.starts_with("Step") || line.starts_with("step") {
1083 headers = line.split_whitespace().map(|s| s.to_lowercase()).collect();
1084 in_thermo = true;
1085 continue;
1086 }
1087 if line.starts_with("Loop") || line.starts_with("WARNING") || line.is_empty() {
1088 if in_thermo && !records.is_empty() {
1089 in_thermo = false;
1090 }
1091 continue;
1092 }
1093 if in_thermo && !headers.is_empty() {
1094 let parts: Vec<&str> = line.split_whitespace().collect();
1095 if parts.len() < headers.len() {
1096 continue;
1097 }
1098 let vals: Vec<Option<f64>> = parts.iter().map(|p| p.parse::<f64>().ok()).collect();
1099 if vals[0].is_none() {
1100 in_thermo = false;
1101 continue;
1102 }
1103 let get = |name: &str| -> Option<f64> {
1104 headers
1105 .iter()
1106 .position(|h| h == name)
1107 .and_then(|i| vals.get(i).and_then(|v| *v))
1108 };
1109 let step = match get("step").map(|v| v as u64) {
1110 Some(s) => s,
1111 None => continue,
1112 };
1113 records.push(LammpsThermoRecord {
1114 step,
1115 temp: get("temp"),
1116 etotal: get("etotal"),
1117 pe: get("pe"),
1118 ke: get("ke"),
1119 press: get("press"),
1120 vol: get("vol"),
1121 });
1122 }
1123 }
1124 records
1125 }
1126 pub fn parse_wall_time(log_text: &str) -> Option<String> {
1131 for line in log_text.lines() {
1132 let t = line.trim();
1133 if t.starts_with("Total wall time:") {
1134 return Some(t.to_string());
1135 }
1136 }
1137 None
1138 }
1139 pub fn count_total_steps(log_text: &str) -> u64 {
1143 let mut total = 0u64;
1144 for line in log_text.lines() {
1145 let t = line.trim().to_lowercase();
1146 if t.starts_with("run ") {
1147 let parts: Vec<&str> = t.split_whitespace().collect();
1148 if parts.len() >= 2
1149 && let Ok(n) = parts[1].parse::<u64>()
1150 {
1151 total += n;
1152 }
1153 }
1154 }
1155 total
1156 }
1157}
1158pub struct LammpsBinaryDumpReader;
1160impl LammpsBinaryDumpReader {
1161 #[allow(clippy::type_complexity)]
1165 pub fn read_frame(
1166 data: &[u8],
1167 offset: usize,
1168 ) -> Result<(i64, [[f64; 2]; 3], Vec<LammpsAtom>, usize)> {
1169 let mut pos = offset;
1170 let timestep = Self::read_i64(data, &mut pos)?;
1171 let n_atoms = Self::read_i64(data, &mut pos)? as usize;
1172 let mut box_bounds = [[0.0_f64; 2]; 3];
1173 for b in &mut box_bounds {
1174 b[0] = Self::read_f64(data, &mut pos)?;
1175 b[1] = Self::read_f64(data, &mut pos)?;
1176 }
1177 let mut atoms = Vec::with_capacity(n_atoms);
1178 for _ in 0..n_atoms {
1179 let id = Self::read_i32(data, &mut pos)? as usize;
1180 let type_id = Self::read_i32(data, &mut pos)? as usize;
1181 let x = Self::read_f64(data, &mut pos)?;
1182 let y = Self::read_f64(data, &mut pos)?;
1183 let z = Self::read_f64(data, &mut pos)?;
1184 let vx = Self::read_f64(data, &mut pos)?;
1185 let vy = Self::read_f64(data, &mut pos)?;
1186 let vz = Self::read_f64(data, &mut pos)?;
1187 atoms.push(LammpsAtom {
1188 id,
1189 type_id,
1190 position: Vec3::new(x, y, z),
1191 velocity: Vec3::new(vx, vy, vz),
1192 });
1193 }
1194 Ok((timestep, box_bounds, atoms, pos - offset))
1195 }
1196 fn read_i32(data: &[u8], pos: &mut usize) -> Result<i32> {
1197 if *pos + 4 > data.len() {
1198 return Err(Error::Parse("binary dump: unexpected EOF (i32)".into()));
1199 }
1200 let v = i32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
1201 *pos += 4;
1202 Ok(v)
1203 }
1204 fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64> {
1205 if *pos + 8 > data.len() {
1206 return Err(Error::Parse("binary dump: unexpected EOF (i64)".into()));
1207 }
1208 let b = &data[*pos..*pos + 8];
1209 let v = i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
1210 *pos += 8;
1211 Ok(v)
1212 }
1213 fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64> {
1214 if *pos + 8 > data.len() {
1215 return Err(Error::Parse("binary dump: unexpected EOF (f64)".into()));
1216 }
1217 let b = &data[*pos..*pos + 8];
1218 let v = f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
1219 *pos += 8;
1220 Ok(v)
1221 }
1222}
1223#[allow(dead_code)]
1225#[derive(Debug, Clone, Default)]
1226pub struct LammpsLJTable {
1227 pub pairs: Vec<LammpsLJPair>,
1229 pub global_cutoff: f64,
1231}
1232#[allow(dead_code)]
1233impl LammpsLJTable {
1234 pub fn new(cutoff: f64) -> Self {
1236 Self {
1237 pairs: Vec::new(),
1238 global_cutoff: cutoff,
1239 }
1240 }
1241 pub fn add_pair(&mut self, pair: LammpsLJPair) {
1243 self.pairs.push(pair);
1244 }
1245 pub fn get(&self, type_i: u32, type_j: u32) -> Option<&LammpsLJPair> {
1247 self.pairs.iter().find(|p| {
1248 (p.type_i == type_i && p.type_j == type_j) || (p.type_i == type_j && p.type_j == type_i)
1249 })
1250 }
1251 pub fn to_lammps_input(&self) -> String {
1253 let mut s = LammpsPairStyle::lj_cut(self.global_cutoff);
1254 for pair in &self.pairs {
1255 s.push_str(&pair.to_pair_coeff_line());
1256 }
1257 s
1258 }
1259 pub fn apply_lorentz_berthelot_mixing(&mut self) {
1266 let like: Vec<(u32, f64, f64)> = self
1267 .pairs
1268 .iter()
1269 .filter(|p| p.type_i == p.type_j)
1270 .map(|p| (p.type_i, p.epsilon, p.sigma))
1271 .collect();
1272 let mut new_pairs = Vec::new();
1273 for i in 0..like.len() {
1274 for j in (i + 1)..like.len() {
1275 let (ti, eps_i, sig_i) = like[i];
1276 let (tj, eps_j, sig_j) = like[j];
1277 if self.get(ti, tj).is_none() {
1278 new_pairs.push(LammpsLJPair {
1279 type_i: ti,
1280 type_j: tj,
1281 epsilon: (eps_i * eps_j).sqrt(),
1282 sigma: (sig_i + sig_j) / 2.0,
1283 cutoff: None,
1284 });
1285 }
1286 }
1287 }
1288 self.pairs.extend(new_pairs);
1289 }
1290}
1291#[allow(dead_code)]
1295#[derive(Debug, Clone, PartialEq)]
1296pub struct LammpsHarmonicAngle {
1297 pub angle_type: u32,
1299 pub k: f64,
1301 pub theta0_deg: f64,
1303}
1304#[allow(dead_code)]
1305impl LammpsHarmonicAngle {
1306 pub fn potential_energy_deg(&self, theta_deg: f64) -> f64 {
1308 let dt = (theta_deg - self.theta0_deg).to_radians();
1309 self.k * dt * dt
1310 }
1311 pub fn to_angle_coeff_line(&self) -> String {
1313 format!(
1314 "angle_coeff {} {} {}\n",
1315 self.angle_type, self.k, self.theta0_deg
1316 )
1317 }
1318}
1319#[allow(dead_code)]
1321pub struct LammpsCompute;
1322#[allow(dead_code)]
1323impl LammpsCompute {
1324 pub fn temp(compute_id: &str, group: &str) -> String {
1326 format!("compute {compute_id} {group} temp\n")
1327 }
1328 pub fn pe(compute_id: &str, group: &str) -> String {
1330 format!("compute {compute_id} {group} pe\n")
1331 }
1332 pub fn ke(compute_id: &str, group: &str) -> String {
1334 format!("compute {compute_id} {group} ke\n")
1335 }
1336 pub fn pressure(compute_id: &str, group: &str, temp_compute: &str) -> String {
1338 format!("compute {compute_id} {group} pressure {temp_compute}\n")
1339 }
1340 pub fn rdf(compute_id: &str, group: &str, n_bins: usize, cutoff: f64) -> String {
1342 format!("compute {compute_id} {group} rdf {n_bins} cutoff {cutoff}\n")
1343 }
1344}
1345pub struct LammpsDumpWriter;
1347impl LammpsDumpWriter {
1348 pub fn write_frame<W: Write>(
1352 writer: &mut W,
1353 timestep: u64,
1354 box_bounds: [[f64; 2]; 3],
1355 atoms: &[LammpsAtom],
1356 ) -> Result<()> {
1357 writeln!(writer, "ITEM: TIMESTEP")?;
1358 writeln!(writer, "{}", timestep)?;
1359 writeln!(writer, "ITEM: NUMBER OF ATOMS")?;
1360 writeln!(writer, "{}", atoms.len())?;
1361 writeln!(writer, "ITEM: BOX BOUNDS pp pp pp")?;
1362 for b in &box_bounds {
1363 writeln!(writer, "{} {}", b[0], b[1])?;
1364 }
1365 writeln!(writer, "ITEM: ATOMS id type x y z vx vy vz")?;
1366 for a in atoms {
1367 writeln!(
1368 writer,
1369 "{} {} {} {} {} {} {} {}",
1370 a.id,
1371 a.type_id,
1372 a.position.x,
1373 a.position.y,
1374 a.position.z,
1375 a.velocity.x,
1376 a.velocity.y,
1377 a.velocity.z,
1378 )?;
1379 }
1380 Ok(())
1381 }
1382}
1383pub struct LammpsRestartReader;
1385impl LammpsRestartReader {
1386 pub fn read(data: &[u8]) -> Result<(i64, Vec<f64>, Vec<LammpsAtom>)> {
1390 if data.len() < 4 || &data[..4] != b"LRST" {
1391 return Err(Error::Parse("restart: missing LRST magic".into()));
1392 }
1393 let mut pos = 4usize;
1394 let _version = read_i32_le(data, &mut pos)?;
1395 let timestep = read_i64_le(data, &mut pos)?;
1396 let n_atoms = read_i64_le(data, &mut pos)? as usize;
1397 let n_types = read_i32_le(data, &mut pos)? as usize;
1398 let mut masses = Vec::with_capacity(n_types);
1399 for _ in 0..n_types {
1400 masses.push(read_f64_le(data, &mut pos)?);
1401 }
1402 let mut atoms = Vec::with_capacity(n_atoms);
1403 for _ in 0..n_atoms {
1404 let id = read_i32_le(data, &mut pos)? as usize;
1405 let type_id = read_i32_le(data, &mut pos)? as usize;
1406 let x = read_f64_le(data, &mut pos)?;
1407 let y = read_f64_le(data, &mut pos)?;
1408 let z = read_f64_le(data, &mut pos)?;
1409 let vx = read_f64_le(data, &mut pos)?;
1410 let vy = read_f64_le(data, &mut pos)?;
1411 let vz = read_f64_le(data, &mut pos)?;
1412 atoms.push(LammpsAtom {
1413 id,
1414 type_id,
1415 position: Vec3::new(x, y, z),
1416 velocity: Vec3::new(vx, vy, vz),
1417 });
1418 }
1419 Ok((timestep, masses, atoms))
1420 }
1421}