Skip to main content

oxiphysics_io/lammps/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![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/// Describes a custom column set for a LAMMPS dump.
12#[allow(dead_code)]
13#[derive(Debug, Clone)]
14pub struct DumpColumnSpec {
15    /// Column names in order.
16    pub columns: Vec<String>,
17}
18impl DumpColumnSpec {
19    /// Standard full column set: `id type x y z vx vy vz`.
20    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    /// Positions only: `id type x y z`.
35    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    /// Return the `ITEM: ATOMS` header line.
47    pub fn header_line(&self) -> String {
48        format!("ITEM: ATOMS {}", self.columns.join(" "))
49    }
50    /// Number of columns.
51    pub fn len(&self) -> usize {
52        self.columns.len()
53    }
54    /// True if no columns are defined.
55    pub fn is_empty(&self) -> bool {
56        self.columns.is_empty()
57    }
58    /// Check if a column name is present.
59    pub fn contains(&self, name: &str) -> bool {
60        self.columns.iter().any(|c| c == name)
61    }
62    /// Index of a column by name.
63    pub fn index_of(&self, name: &str) -> Option<usize> {
64        self.columns.iter().position(|c| c == name)
65    }
66}
67/// LAMMPS-style restart file: stores atom positions and velocities in a
68/// compact binary format for simulation restart.
69pub struct LammpsRestartWriter;
70impl LammpsRestartWriter {
71    /// Write restart data to `writer`.
72    ///
73    /// Layout:
74    /// ```text
75    /// [magic: b"LRST"] 4 bytes
76    /// [version: i32 LE = 1]
77    /// [timestep: i64 LE]
78    /// [n_atoms: i64 LE]
79    /// [n_types: i32 LE]
80    /// [masses: n_types × f64 LE]
81    /// for each atom: [id i32][type i32][x f64][y f64][z f64][vx f64][vy f64][vz f64]
82    /// ```
83    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(&timestep.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/// Simulation box geometry for a LAMMPS run.
111#[allow(dead_code)]
112#[derive(Debug, Clone)]
113pub struct LammpsBox {
114    /// Low corner `[xlo, ylo, zlo]`.
115    pub lo: [f64; 3],
116    /// High corner `[xhi, yhi, zhi]`.
117    pub hi: [f64; 3],
118    /// Tilt factors `[xy, xz, yz]` for triclinic cells (0 for orthogonal).
119    pub tilt: [f64; 3],
120}
121#[allow(dead_code)]
122impl LammpsBox {
123    /// Create an orthogonal box with given extents.
124    pub fn orthogonal(lo: [f64; 3], hi: [f64; 3]) -> Self {
125        Self {
126            lo,
127            hi,
128            tilt: [0.0; 3],
129        }
130    }
131    /// Box lengths in each dimension.
132    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    /// Volume of an orthogonal box.
140    pub fn volume(&self) -> f64 {
141        let l = self.lengths();
142        l[0] * l[1] * l[2]
143    }
144    /// Number density given an atom count.
145    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    /// Generate LAMMPS `region` command for a box region.
153    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    /// Check whether a position lies inside the box (for orthogonal boxes).
160    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    /// Wrap a position into the primary box using periodic boundary conditions.
164    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/// Generator for common LAMMPS input-script sections.
176#[allow(dead_code)]
177pub struct LammpsInputScript;
178#[allow(dead_code)]
179impl LammpsInputScript {
180    /// Generate a minimisation script section.
181    ///
182    /// * `force_tol`  — force convergence tolerance (eV/Å or kcal/mol/Å).
183    /// * `energy_tol` — energy convergence tolerance.
184    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    /// Generate an NPT run script section.
188    ///
189    /// * `temp`    — target temperature (K).
190    /// * `press`   — target pressure (bar).
191    /// * `t_damp`  — temperature damping parameter (time units).
192    /// * `p_damp`  — pressure damping parameter (time units).
193    /// * `n_steps` — number of MD steps.
194    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    /// Generate a complete NVE run section.
203    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    /// Generate a thermalisation script with Langevin dynamics.
212    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    /// Generate a dump command.
224    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    /// Generate a thermo output command.
235    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    /// Generate a units command.
243    pub fn units(style: &str) -> String {
244        format!("units {style}\n")
245    }
246    /// Generate a boundary command.
247    pub fn boundary(style: &str) -> String {
248        format!("boundary {style}\n")
249    }
250    /// Generate a read_data command.
251    pub fn read_data(filename: &str) -> String {
252        format!("read_data {filename}\n")
253    }
254}
255/// Writer for LAMMPS binary dump format (little-endian).
256///
257/// Produces a compact binary representation of a single frame with the
258/// standard `id type x y z vx vy vz` per-atom layout.
259pub struct LammpsBinaryDumpWriter;
260impl LammpsBinaryDumpWriter {
261    /// Write one binary frame to `writer`.
262    ///
263    /// Layout per frame:
264    /// ```text
265    /// [timestep: i64 LE]
266    /// [n_atoms:  i64 LE]
267    /// [6 × f64 LE: xlo, xhi, ylo, yhi, zlo, zhi]
268    /// for each atom:
269    ///   [id:    i32 LE]
270    ///   [type:  i32 LE]
271    ///   [x y z vx vy vz: 6 × f64 LE]
272    /// ```
273    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(&timestep.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    /// Byte size of one binary frame (header + atoms).
298    pub fn frame_byte_size(n_atoms: usize) -> usize {
299        8 + 8 + 6 * 8 + n_atoms * (4 + 4 + 6 * 8)
300    }
301}
302/// Writer for the LAMMPS data-file format (used with `read_data`).
303#[allow(dead_code)]
304pub struct LammpsDataWriter;
305#[allow(dead_code)]
306impl LammpsDataWriter {
307    /// Generate the header section of a LAMMPS data file.
308    ///
309    /// `box_lo` / `box_hi` are the lower/upper corners of the simulation box.
310    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    /// Generate the `Atoms` section for `atom_style atomic`.
329    ///
330    /// `types` is 1-indexed atom type per atom.
331    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    /// Generate the `Atoms` section for `atom_style charge`.
347    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    /// Generate the `Bonds` section.
374    ///
375    /// Each entry is `(bond_type, atom_i, atom_j)` with 1-indexed atoms.
376    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    /// Generate a complete LAMMPS data file string (atomic style, no bonds).
384    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    /// Generate the `Atoms` section for `atom_style full`.
409    ///
410    /// Format: `id mol-id type charge x y z`
411    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    /// Generate the `Atoms` section for `atom_style molecular`.
439    ///
440    /// Format: `id mol-id type x y z`
441    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/// Reader for the LAMMPS data-file format.
463#[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    /// Parse a LAMMPS data file from a string.
474    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    /// Return parsed positions.
583    pub fn positions(&self) -> &[[f64; 3]] {
584        &self.positions
585    }
586    /// Return per-atom masses.
587    pub fn masses(&self) -> &[f64] {
588        &self.masses
589    }
590    /// Return per-atom type IDs (1-indexed).
591    pub fn types(&self) -> &[u32] {
592        &self.types
593    }
594    /// Return the simulation box bounds as `(lo, hi)`.
595    pub fn box_bounds(&self) -> ([f64; 3], [f64; 3]) {
596        (self.box_lo, self.box_hi)
597    }
598}
599/// Represents a single atom in a LAMMPS dump frame.
600#[derive(Debug, Clone)]
601pub struct LammpsAtom {
602    /// Atom ID.
603    pub id: usize,
604    /// Atom type ID.
605    pub type_id: usize,
606    /// 3D position.
607    pub position: Vec3,
608    /// 3D velocity.
609    pub velocity: Vec3,
610}
611/// Generator for LAMMPS fix commands.
612#[allow(dead_code)]
613pub struct LammpsFix;
614#[allow(dead_code)]
615impl LammpsFix {
616    /// Generate an NVE fix.
617    pub fn nve(fix_id: &str, group: &str) -> String {
618        format!("fix {fix_id} {group} nve\n")
619    }
620    /// Generate an NVT (Nose-Hoover thermostat) fix.
621    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    /// Generate a Langevin thermostat fix.
625    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    /// Generate a wall/reflect fix.
636    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    /// Generate a `fix setforce` command.
641    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    /// Generate a `fix rigid` command.
645    pub fn rigid(fix_id: &str, group: &str, style: &str) -> String {
646        format!("fix {fix_id} {group} rigid {style}\n")
647    }
648}
649/// Lennard-Jones pair interaction parameters for one type pair.
650#[allow(dead_code)]
651#[derive(Debug, Clone, PartialEq)]
652pub struct LammpsLJPair {
653    /// Type index I (1-based).
654    pub type_i: u32,
655    /// Type index J (1-based).
656    pub type_j: u32,
657    /// Well depth epsilon (energy units).
658    pub epsilon: f64,
659    /// Diameter sigma (distance units).
660    pub sigma: f64,
661    /// Cutoff distance. If `None`, uses the global cutoff.
662    pub cutoff: Option<f64>,
663}
664#[allow(dead_code)]
665impl LammpsLJPair {
666    /// Compute the 12-6 LJ potential energy at separation `r`.
667    ///
668    /// `U(r) = 4ε[(σ/r)^12 − (σ/r)^6]`
669    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    /// Compute the magnitude of the 12-6 LJ force at separation `r`.
676    ///
677    /// `F(r) = 24ε/r [2(σ/r)^12 − (σ/r)^6]`
678    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    /// Return the distance where the LJ potential is at its minimum (r_min = 2^(1/6) σ).
685    pub fn r_min(&self) -> f64 {
686        2.0_f64.powf(1.0 / 6.0) * self.sigma
687    }
688    /// Write LAMMPS `pair_coeff` line for this pair.
689    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/// A single thermo output record from a LAMMPS log file.
704#[allow(dead_code)]
705#[derive(Debug, Clone)]
706pub struct LammpsThermoRecord {
707    /// Step number.
708    pub step: u64,
709    /// Temperature (K), if present.
710    pub temp: Option<f64>,
711    /// Total energy (energy units), if present.
712    pub etotal: Option<f64>,
713    /// Potential energy, if present.
714    pub pe: Option<f64>,
715    /// Kinetic energy, if present.
716    pub ke: Option<f64>,
717    /// Pressure (pressure units), if present.
718    pub press: Option<f64>,
719    /// Volume, if present.
720    pub vol: Option<f64>,
721}
722/// Builder for the coefficient sections of a LAMMPS data file.
723///
724/// Generates the `Masses`, `Pair Coeffs`, and `Bond Coeffs` sections.
725#[allow(dead_code)]
726pub struct LammpsDataSectionBuilder {
727    /// (type_id, mass)
728    pub masses: Vec<(usize, f64)>,
729    /// (type_i, type_j, epsilon, sigma)
730    pub pair_coeffs: Vec<(usize, usize, f64, f64)>,
731    /// (bond_type, k, r0)
732    pub bond_coeffs: Vec<(usize, f64, f64)>,
733    /// (angle_type, k, theta0_deg)
734    pub angle_coeffs: Vec<(usize, f64, f64)>,
735}
736impl LammpsDataSectionBuilder {
737    /// Create an empty builder.
738    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    /// Add a mass entry.
747    pub fn add_mass(&mut self, type_id: usize, mass: f64) {
748        self.masses.push((type_id, mass));
749    }
750    /// Add a Lennard-Jones pair coefficient.
751    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    /// Add a harmonic bond coefficient.
755    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    /// Add a harmonic angle coefficient.
759    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    /// Generate the `Masses` section text.
763    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    /// Generate the `Pair Coeffs` section text.
771    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    /// Generate the `Bond Coeffs` section text.
779    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    /// Generate the `Angle Coeffs` section text.
787    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    /// Combine all sections into one string.
795    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/// Harmonic bond potential parameters.
817///
818/// `U(r) = K (r − r₀)²`
819#[allow(dead_code)]
820#[derive(Debug, Clone, PartialEq)]
821pub struct LammpsHarmonicBond {
822    /// Bond type index (1-based).
823    pub bond_type: u32,
824    /// Spring constant K (energy/distance²).
825    pub k: f64,
826    /// Equilibrium bond length râ‚€.
827    pub r0: f64,
828}
829#[allow(dead_code)]
830impl LammpsHarmonicBond {
831    /// Compute bond potential energy.
832    pub fn potential_energy(&self, r: f64) -> f64 {
833        self.k * (r - self.r0) * (r - self.r0)
834    }
835    /// Compute bond force magnitude (restoring).
836    pub fn force_magnitude(&self, r: f64) -> f64 {
837        2.0 * self.k * (r - self.r0)
838    }
839    /// Generate LAMMPS `bond_coeff` line.
840    pub fn to_bond_coeff_line(&self) -> String {
841        format!("bond_coeff {} {} {}\n", self.bond_type, self.k, self.r0)
842    }
843}
844/// Builder for a LAMMPS input script `run` / `timestep` / `fix` block.
845#[allow(dead_code)]
846pub struct LammpsRunBlock {
847    /// Integration timestep in fs.
848    pub timestep: f64,
849    /// Number of MD steps.
850    pub run_steps: u64,
851    /// Thermo output frequency.
852    pub thermo_freq: u64,
853    /// Dump output frequency.
854    pub dump_freq: u64,
855    /// Ensemble: "nve", "nvt", "npt".
856    pub ensemble: String,
857    /// Temperature target (for NVT/NPT).
858    pub temp_target: f64,
859    /// Pressure target in bar (for NPT).
860    pub pressure_target: Option<f64>,
861}
862impl LammpsRunBlock {
863    /// Create a new NVE run block.
864    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    /// Create a new NVT run block.
876    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    /// Create a new NPT run block.
888    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    /// Set thermo output frequency.
900    pub fn thermo_every(mut self, freq: u64) -> Self {
901        self.thermo_freq = freq;
902        self
903    }
904    /// Set dump frequency.
905    pub fn dump_every(mut self, freq: u64) -> Self {
906        self.dump_freq = freq;
907        self
908    }
909    /// Total simulated time in fs.
910    pub fn total_time_fs(&self) -> f64 {
911        self.timestep * self.run_steps as f64
912    }
913    /// Generate the LAMMPS input script block.
914    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}
947/// Reader for LAMMPS custom dump format.
948pub struct LammpsDumpReader;
949impl LammpsDumpReader {
950    /// Read a single frame from a LAMMPS dump stream.
951    ///
952    /// Returns `(timestep, atoms)`. The stream must start at the beginning of a frame.
953    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/// Generator for LAMMPS pair style commands.
1026#[allow(dead_code)]
1027pub struct LammpsPairStyle;
1028#[allow(dead_code)]
1029impl LammpsPairStyle {
1030    /// Generate a `pair_style lj/cut` command.
1031    pub fn lj_cut(cutoff: f64) -> String {
1032        format!("pair_style lj/cut {cutoff}\n")
1033    }
1034    /// Generate a `pair_coeff` command for LJ parameters.
1035    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    /// Generate a `pair_style lj/cut/coul/long` command (for charged systems).
1039    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    /// Generate a `pair_style hybrid` command.
1043    pub fn hybrid(styles: &[&str]) -> String {
1044        format!("pair_style hybrid {}\n", styles.join(" "))
1045    }
1046    /// Generate a `pair_modify` command.
1047    pub fn pair_modify(options: &str) -> String {
1048        format!("pair_modify {options}\n")
1049    }
1050}
1051/// Atom style for a LAMMPS data file.
1052#[allow(dead_code)]
1053#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1054pub enum LammpsAtomStyle {
1055    /// `atomic` — id type x y z
1056    Atomic,
1057    /// `full` — id mol-id type charge x y z
1058    Full,
1059    /// `molecular` — id mol-id type x y z
1060    Molecular,
1061    /// `charge` — id type charge x y z
1062    Charge,
1063}
1064/// Parse LAMMPS thermo output from a log file string.
1065///
1066/// Looks for lines starting with a header of whitespace-separated column names,
1067/// then reads subsequent numeric rows.
1068#[allow(dead_code)]
1069pub struct LammpsLogParser;
1070#[allow(dead_code)]
1071impl LammpsLogParser {
1072    /// Parse thermo records from a LAMMPS log file string.
1073    ///
1074    /// Returns a vector of thermo records. Columns are matched by name (case-insensitive).
1075    /// Unknown columns are ignored.
1076    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    /// Extract the total simulation time from a log file.
1127    ///
1128    /// Looks for a line of the form `Total wall time: HH:MM:SS`.
1129    /// Returns `None` if not found.
1130    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    /// Count the number of MD steps in a LAMMPS log file.
1140    ///
1141    /// Looks for lines matching `run N` and sums up N.
1142    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}
1158/// Reader for the binary dump format written by [`LammpsBinaryDumpWriter`].
1159pub struct LammpsBinaryDumpReader;
1160impl LammpsBinaryDumpReader {
1161    /// Read one binary frame from the byte slice starting at `offset`.
1162    ///
1163    /// Returns `(timestep, box_bounds, atoms, bytes_consumed)` on success.
1164    #[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/// A table of LJ pair parameters for a simulation.
1224#[allow(dead_code)]
1225#[derive(Debug, Clone, Default)]
1226pub struct LammpsLJTable {
1227    /// All pair interactions.
1228    pub pairs: Vec<LammpsLJPair>,
1229    /// Global LJ cutoff (distance units).
1230    pub global_cutoff: f64,
1231}
1232#[allow(dead_code)]
1233impl LammpsLJTable {
1234    /// Create an empty table with a given global cutoff.
1235    pub fn new(cutoff: f64) -> Self {
1236        Self {
1237            pairs: Vec::new(),
1238            global_cutoff: cutoff,
1239        }
1240    }
1241    /// Add a pair interaction.
1242    pub fn add_pair(&mut self, pair: LammpsLJPair) {
1243        self.pairs.push(pair);
1244    }
1245    /// Look up parameters for types (i, j). Symmetric lookup.
1246    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    /// Generate `pair_style lj/cut` + all `pair_coeff` lines.
1252    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    /// Apply Lorentz-Berthelot mixing rules to fill in cross-interactions.
1260    ///
1261    /// For types `(i, j)` where `i != j`, if no cross-term exists,
1262    /// create one from the like-pairs using:
1263    /// - `ε_ij = sqrt(ε_ii * ε_jj)`
1264    /// - `σ_ij = (σ_ii + σ_jj) / 2`
1265    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/// Harmonic angle potential parameters.
1292///
1293/// `U(θ) = K (θ − θ₀)²`
1294#[allow(dead_code)]
1295#[derive(Debug, Clone, PartialEq)]
1296pub struct LammpsHarmonicAngle {
1297    /// Angle type index (1-based).
1298    pub angle_type: u32,
1299    /// Spring constant K (energy/rad²).
1300    pub k: f64,
1301    /// Equilibrium angle θ₀ (degrees).
1302    pub theta0_deg: f64,
1303}
1304#[allow(dead_code)]
1305impl LammpsHarmonicAngle {
1306    /// Compute angle potential energy given `theta` in degrees.
1307    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    /// Generate LAMMPS `angle_coeff` line.
1312    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/// Generator for LAMMPS compute commands.
1320#[allow(dead_code)]
1321pub struct LammpsCompute;
1322#[allow(dead_code)]
1323impl LammpsCompute {
1324    /// Generate a `compute` for temperature.
1325    pub fn temp(compute_id: &str, group: &str) -> String {
1326        format!("compute {compute_id} {group} temp\n")
1327    }
1328    /// Generate a `compute` for potential energy.
1329    pub fn pe(compute_id: &str, group: &str) -> String {
1330        format!("compute {compute_id} {group} pe\n")
1331    }
1332    /// Generate a `compute` for kinetic energy.
1333    pub fn ke(compute_id: &str, group: &str) -> String {
1334        format!("compute {compute_id} {group} ke\n")
1335    }
1336    /// Generate a `compute` for pressure.
1337    pub fn pressure(compute_id: &str, group: &str, temp_compute: &str) -> String {
1338        format!("compute {compute_id} {group} pressure {temp_compute}\n")
1339    }
1340    /// Generate a `compute` for RDF.
1341    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}
1345/// Writer for LAMMPS custom dump format.
1346pub struct LammpsDumpWriter;
1347impl LammpsDumpWriter {
1348    /// Write a single frame in LAMMPS custom dump format.
1349    ///
1350    /// `box_bounds` is `[[xlo, xhi\], [ylo, yhi], [zlo, zhi]]`.
1351    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}
1383/// Reader for LAMMPS restart files written by [`LammpsRestartWriter`].
1384pub struct LammpsRestartReader;
1385impl LammpsRestartReader {
1386    /// Parse a restart file from bytes.
1387    ///
1388    /// Returns `(timestep, masses, atoms)` on success.
1389    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}