1#![allow(clippy::should_implement_trait)]
2use std::io::{BufRead, Write};
26
27#[derive(Debug, Clone)]
29pub struct LammpsDumpFrame {
30 pub timestep: u64,
32 pub n_atoms: usize,
34 pub box_bounds: [[f64; 2]; 3],
36 pub atom_ids: Vec<u32>,
38 pub types: Vec<u32>,
40 pub positions: Vec<[f64; 3]>,
42 pub velocities: Option<Vec<[f64; 3]>>,
44 pub tilt_factors: Option<[f64; 3]>,
46 pub custom_properties: Vec<(String, Vec<f64>)>,
48}
49
50impl LammpsDumpFrame {
51 #[allow(dead_code)]
53 pub fn positions_by_type(&self, atom_type: u32) -> Vec<[f64; 3]> {
54 self.types
55 .iter()
56 .enumerate()
57 .filter_map(|(i, &t)| {
58 if t == atom_type {
59 Some(self.positions[i])
60 } else {
61 None
62 }
63 })
64 .collect()
65 }
66
67 #[allow(dead_code)]
69 pub fn count_by_type(&self, atom_type: u32) -> usize {
70 self.types.iter().filter(|&&t| t == atom_type).count()
71 }
72
73 #[allow(dead_code)]
75 pub fn box_dimensions(&self) -> [f64; 3] {
76 [
77 self.box_bounds[0][1] - self.box_bounds[0][0],
78 self.box_bounds[1][1] - self.box_bounds[1][0],
79 self.box_bounds[2][1] - self.box_bounds[2][0],
80 ]
81 }
82
83 #[allow(dead_code)]
85 pub fn box_center(&self) -> [f64; 3] {
86 [
87 (self.box_bounds[0][0] + self.box_bounds[0][1]) * 0.5,
88 (self.box_bounds[1][0] + self.box_bounds[1][1]) * 0.5,
89 (self.box_bounds[2][0] + self.box_bounds[2][1]) * 0.5,
90 ]
91 }
92
93 #[allow(dead_code)]
95 pub fn box_volume(&self) -> f64 {
96 let dims = self.box_dimensions();
97 dims[0] * dims[1] * dims[2]
98 }
99
100 #[allow(dead_code)]
102 pub fn center_of_mass(&self) -> [f64; 3] {
103 if self.positions.is_empty() {
104 return [0.0; 3];
105 }
106 let n = self.positions.len() as f64;
107 let mut com = [0.0; 3];
108 for p in &self.positions {
109 com[0] += p[0];
110 com[1] += p[1];
111 com[2] += p[2];
112 }
113 [com[0] / n, com[1] / n, com[2] / n]
114 }
115
116 #[allow(dead_code)]
118 pub fn get_custom_property(&self, name: &str) -> Option<&[f64]> {
119 self.custom_properties
120 .iter()
121 .find(|(n, _)| n == name)
122 .map(|(_, v)| v.as_slice())
123 }
124
125 #[allow(dead_code)]
127 pub fn atom_types(&self) -> Vec<u32> {
128 let mut types: Vec<u32> = self.types.clone();
129 types.sort_unstable();
130 types.dedup();
131 types
132 }
133}
134
135pub struct LammpsDumpReader;
137
138impl LammpsDumpReader {
139 pub fn read_frame(reader: impl BufRead) -> Option<Result<LammpsDumpFrame, String>> {
144 let mut lines = reader.lines().peekable();
145
146 loop {
148 match lines.peek() {
149 None => return None,
150 Some(Ok(l)) if l.trim().is_empty() => {
151 lines.next();
152 }
153 Some(Err(_)) => {
154 let e = lines
155 .next()
156 .expect("iterator should have elements")
157 .expect_err("matched Err(_) arm so value must be Err");
158 return Some(Err(e.to_string()));
159 }
160 _ => break,
161 }
162 }
163
164 macro_rules! next_line {
166 ($label:expr) => {{
167 match lines.next() {
168 None => return Some(Err(format!("LAMMPS dump: unexpected EOF at {}", $label))),
169 Some(Err(e)) => return Some(Err(e.to_string())),
170 Some(Ok(l)) => l,
171 }
172 }};
173 }
174
175 macro_rules! expect_keyword {
176 ($kw:expr) => {{
177 let l = next_line!($kw);
178 if !l.trim().starts_with($kw) {
179 return Some(Err(format!(
180 "LAMMPS dump: expected '{}', got '{}'",
181 $kw,
182 l.trim()
183 )));
184 }
185 l
186 }};
187 }
188 expect_keyword!("ITEM: TIMESTEP");
191 let ts_str = next_line!("timestep value");
192 let timestep: u64 = match ts_str.trim().parse() {
193 Ok(v) => v,
194 Err(e) => return Some(Err(format!("LAMMPS dump: bad timestep: {e}"))),
195 };
196
197 expect_keyword!("ITEM: NUMBER OF ATOMS");
198 let na_str = next_line!("n_atoms value");
199 let n_atoms: usize = match na_str.trim().parse() {
200 Ok(v) => v,
201 Err(e) => return Some(Err(format!("LAMMPS dump: bad n_atoms: {e}"))),
202 };
203
204 let box_header = expect_keyword!("ITEM: BOX BOUNDS");
205 let is_triclinic = box_header.contains("xy xz yz");
206
207 let mut box_bounds = [[0.0f64; 2]; 3];
208 let mut tilt_factors: Option<[f64; 3]> = None;
209
210 if is_triclinic {
211 let mut tilts = [0.0f64; 3];
212 for (i, row) in box_bounds.iter_mut().enumerate() {
213 let bl = next_line!(format!("box bound {i}"));
214 let parts: Vec<&str> = bl.split_whitespace().collect();
215 if parts.len() < 3 {
216 return Some(Err(format!(
217 "LAMMPS dump: triclinic box line {i} too short"
218 )));
219 }
220 let lo: f64 = match parts[0].parse() {
221 Ok(v) => v,
222 Err(e) => return Some(Err(format!("LAMMPS dump: box lo[{i}]: {e}"))),
223 };
224 let hi: f64 = match parts[1].parse() {
225 Ok(v) => v,
226 Err(e) => return Some(Err(format!("LAMMPS dump: box hi[{i}]: {e}"))),
227 };
228 let tilt: f64 = match parts[2].parse() {
229 Ok(v) => v,
230 Err(e) => return Some(Err(format!("LAMMPS dump: tilt[{i}]: {e}"))),
231 };
232 *row = [lo, hi];
233 tilts[i] = tilt;
234 }
235 tilt_factors = Some(tilts);
236 } else {
237 for (i, row) in box_bounds.iter_mut().enumerate() {
238 let bl = next_line!(format!("box bound {i}"));
239 let parts: Vec<&str> = bl.split_whitespace().collect();
240 if parts.len() < 2 {
241 return Some(Err(format!("LAMMPS dump: box line {i} too short")));
242 }
243 let lo: f64 = match parts[0].parse() {
244 Ok(v) => v,
245 Err(e) => return Some(Err(format!("LAMMPS dump: box lo[{i}]: {e}"))),
246 };
247 let hi: f64 = match parts[1].parse() {
248 Ok(v) => v,
249 Err(e) => return Some(Err(format!("LAMMPS dump: box hi[{i}]: {e}"))),
250 };
251 *row = [lo, hi];
252 }
253 }
254
255 let header_line = expect_keyword!("ITEM: ATOMS");
257 let has_velocities = header_line.contains("vx");
258
259 let col_names: Vec<&str> = header_line
261 .trim()
262 .strip_prefix("ITEM: ATOMS")
263 .unwrap_or("")
264 .split_whitespace()
265 .collect();
266
267 let standard_cols = ["id", "type", "x", "y", "z", "vx", "vy", "vz"];
269 let custom_col_indices: Vec<(usize, String)> = col_names
270 .iter()
271 .enumerate()
272 .filter(|(_, name)| !standard_cols.contains(name))
273 .map(|(i, name)| (i, name.to_string()))
274 .collect();
275
276 let mut atom_ids = Vec::with_capacity(n_atoms);
277 let mut types = Vec::with_capacity(n_atoms);
278 let mut positions = Vec::with_capacity(n_atoms);
279 let mut velocities: Vec<[f64; 3]> = if has_velocities {
280 Vec::with_capacity(n_atoms)
281 } else {
282 Vec::new()
283 };
284 let mut custom_data: Vec<Vec<f64>> = custom_col_indices
285 .iter()
286 .map(|_| Vec::with_capacity(n_atoms))
287 .collect();
288
289 for i in 0..n_atoms {
290 let al = next_line!(format!("atom {i}"));
291 let parts: Vec<&str> = al.split_whitespace().collect();
292 let min_cols = if has_velocities { 8 } else { 5 };
293 if parts.len() < min_cols {
294 return Some(Err(format!(
295 "LAMMPS dump: atom line {i} has {} cols, need {}",
296 parts.len(),
297 min_cols
298 )));
299 }
300 macro_rules! parse_col {
301 ($idx:expr, $ty:ty, $label:expr) => {
302 match parts[$idx].parse::<$ty>() {
303 Ok(v) => v,
304 Err(e) => {
305 return Some(Err(format!(
306 "LAMMPS dump: atom {i} col {} ({}): {e}",
307 $idx, $label
308 )))
309 }
310 }
311 };
312 }
313 let id = parse_col!(0, u32, "id");
314 let ty = parse_col!(1, u32, "type");
315 let x = parse_col!(2, f64, "x");
316 let y = parse_col!(3, f64, "y");
317 let z = parse_col!(4, f64, "z");
318 atom_ids.push(id);
319 types.push(ty);
320 positions.push([x, y, z]);
321 if has_velocities {
322 let vx = parse_col!(5, f64, "vx");
323 let vy = parse_col!(6, f64, "vy");
324 let vz = parse_col!(7, f64, "vz");
325 velocities.push([vx, vy, vz]);
326 }
327
328 for (ci, (col_idx, _)) in custom_col_indices.iter().enumerate() {
330 if let Some(val_str) = parts.get(*col_idx) {
331 let val = val_str.parse::<f64>().unwrap_or(0.0);
332 custom_data[ci].push(val);
333 }
334 }
335 }
336
337 let custom_properties: Vec<(String, Vec<f64>)> = custom_col_indices
338 .into_iter()
339 .zip(custom_data)
340 .map(|((_, name), data)| (name, data))
341 .collect();
342
343 Some(Ok(LammpsDumpFrame {
344 timestep,
345 n_atoms,
346 box_bounds,
347 atom_ids,
348 types,
349 positions,
350 velocities: if has_velocities {
351 Some(velocities)
352 } else {
353 None
354 },
355 tilt_factors,
356 custom_properties,
357 }))
358 }
359
360 #[allow(dead_code)]
362 pub fn read_all_frames(content: &str) -> Result<Vec<LammpsDumpFrame>, String> {
363 let mut frames = Vec::new();
364 let chunks: Vec<&str> = content
366 .split("ITEM: TIMESTEP\n")
367 .filter(|s| !s.trim().is_empty())
368 .collect();
369
370 for chunk in chunks {
371 let full = format!("ITEM: TIMESTEP\n{chunk}");
372 let cursor = std::io::Cursor::new(full.as_bytes());
373 match Self::read_frame(cursor) {
374 Some(Ok(frame)) => frames.push(frame),
375 Some(Err(e)) => return Err(e),
376 None => {}
377 }
378 }
379 Ok(frames)
380 }
381
382 #[allow(dead_code)]
384 pub fn extract_timesteps(content: &str) -> Vec<u64> {
385 let mut timesteps = Vec::new();
386 let mut lines = content.lines();
387 while let Some(line) = lines.next() {
388 if line.trim() == "ITEM: TIMESTEP"
389 && let Some(ts_line) = lines.next()
390 && let Ok(ts) = ts_line.trim().parse::<u64>()
391 {
392 timesteps.push(ts);
393 }
394 }
395 timesteps
396 }
397}
398
399pub struct LammpsDumpWriter;
401
402impl LammpsDumpWriter {
403 pub fn write_frame(mut writer: impl Write, frame: &LammpsDumpFrame) -> Result<(), String> {
405 writeln!(writer, "ITEM: TIMESTEP").map_err(|e| e.to_string())?;
406 writeln!(writer, "{}", frame.timestep).map_err(|e| e.to_string())?;
407 writeln!(writer, "ITEM: NUMBER OF ATOMS").map_err(|e| e.to_string())?;
408 writeln!(writer, "{}", frame.n_atoms).map_err(|e| e.to_string())?;
409
410 if let Some(tilts) = frame.tilt_factors {
411 writeln!(writer, "ITEM: BOX BOUNDS xy xz yz pp pp pp").map_err(|e| e.to_string())?;
412 for (i, b) in frame.box_bounds.iter().enumerate() {
413 writeln!(writer, "{} {} {}", b[0], b[1], tilts[i]).map_err(|e| e.to_string())?;
414 }
415 } else {
416 writeln!(writer, "ITEM: BOX BOUNDS pp pp pp").map_err(|e| e.to_string())?;
417 for b in &frame.box_bounds {
418 writeln!(writer, "{} {}", b[0], b[1]).map_err(|e| e.to_string())?;
419 }
420 }
421
422 let has_vel = frame.velocities.is_some();
423 let custom_names: Vec<&str> = frame
424 .custom_properties
425 .iter()
426 .map(|(n, _)| n.as_str())
427 .collect();
428
429 let mut header = String::from("ITEM: ATOMS id type x y z");
431 if has_vel {
432 header.push_str(" vx vy vz");
433 }
434 for name in &custom_names {
435 header.push(' ');
436 header.push_str(name);
437 }
438 writeln!(writer, "{}", header).map_err(|e| e.to_string())?;
439
440 let vels = frame.velocities.as_deref();
441 for i in 0..frame.n_atoms {
442 let [x, y, z] = frame.positions[i];
443 let mut line = if has_vel {
444 let [vx, vy, vz] = vels.expect("value should be present")[i];
445 format!(
446 "{} {} {} {} {} {} {} {}",
447 frame.atom_ids[i], frame.types[i], x, y, z, vx, vy, vz,
448 )
449 } else {
450 format!("{} {} {} {} {}", frame.atom_ids[i], frame.types[i], x, y, z,)
451 };
452
453 for (_, values) in &frame.custom_properties {
454 if i < values.len() {
455 line.push_str(&format!(" {}", values[i]));
456 }
457 }
458
459 writeln!(writer, "{}", line).map_err(|e| e.to_string())?;
460 }
461 Ok(())
462 }
463
464 #[allow(dead_code)]
466 pub fn write_frames(writer: impl Write, frames: &[LammpsDumpFrame]) -> Result<(), String> {
467 let mut writer = writer;
468 for frame in frames {
469 Self::write_frame(&mut writer, frame)?;
470 }
471 Ok(())
472 }
473}
474
475#[allow(dead_code)]
477pub fn number_density(frame: &LammpsDumpFrame) -> f64 {
478 let vol = frame.box_volume();
479 if vol.abs() < 1e-30 {
480 return 0.0;
481 }
482 frame.n_atoms as f64 / vol
483}
484
485#[allow(dead_code)]
488pub fn mean_kinetic_energy(frame: &LammpsDumpFrame) -> Option<f64> {
489 let vels = frame.velocities.as_ref()?;
490 if vels.is_empty() {
491 return Some(0.0);
492 }
493 let total: f64 = vels
494 .iter()
495 .map(|v| 0.5 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]))
496 .sum();
497 Some(total / vels.len() as f64)
498}
499
500#[allow(dead_code)]
507pub fn extract_stress_tensor(frame: &LammpsDumpFrame) -> Option<Vec<[f64; 6]>> {
508 let names = ["sxx", "syy", "szz", "sxy", "sxz", "syz"];
510 let props: Vec<Option<&[f64]>> = names.iter().map(|n| frame.get_custom_property(n)).collect();
511
512 if props.iter().all(|p| p.is_some()) {
513 let n = frame.n_atoms;
514 let result: Vec<[f64; 6]> = (0..n)
515 .map(|i| {
516 [
517 props[0].expect("value should be present")[i],
518 props[1].expect("value should be present")[i],
519 props[2].expect("value should be present")[i],
520 props[3].expect("value should be present")[i],
521 props[4].expect("value should be present")[i],
522 props[5].expect("value should be present")[i],
523 ]
524 })
525 .collect();
526 return Some(result);
527 }
528
529 None
530}
531
532#[allow(dead_code)]
534pub fn von_mises_stress(s: [f64; 6]) -> f64 {
535 let [s11, s22, s33, s12, s13, s23] = s;
536 (0.5 * ((s11 - s22).powi(2) + (s22 - s33).powi(2) + (s33 - s11).powi(2))
537 + 3.0 * (s12 * s12 + s13 * s13 + s23 * s23))
538 .sqrt()
539}
540
541#[allow(dead_code)]
543pub fn hydrostatic_pressure(s: [f64; 6]) -> f64 {
544 -(s[0] + s[1] + s[2]) / 3.0
545}
546
547pub struct DumpTimeSeries {
551 pub frames: Vec<LammpsDumpFrame>,
553}
554
555impl DumpTimeSeries {
556 #[allow(dead_code)]
558 pub fn new(frames: Vec<LammpsDumpFrame>) -> Self {
559 Self { frames }
560 }
561
562 #[allow(dead_code)]
564 pub fn from_str(content: &str) -> Result<Self, String> {
565 let frames = LammpsDumpReader::read_all_frames(content)?;
566 Ok(Self { frames })
567 }
568
569 #[allow(dead_code)]
571 pub fn len(&self) -> usize {
572 self.frames.len()
573 }
574
575 #[allow(dead_code)]
577 pub fn is_empty(&self) -> bool {
578 self.frames.is_empty()
579 }
580
581 #[allow(dead_code)]
583 pub fn timesteps(&self) -> Vec<u64> {
584 self.frames.iter().map(|f| f.timestep).collect()
585 }
586
587 #[allow(dead_code)]
589 pub fn mean_ke_series(&self) -> Vec<Option<f64>> {
590 self.frames.iter().map(mean_kinetic_energy).collect()
591 }
592
593 #[allow(dead_code)]
595 pub fn number_density_series(&self) -> Vec<f64> {
596 self.frames.iter().map(number_density).collect()
597 }
598
599 #[allow(dead_code)]
601 pub fn frame_at_timestep(&self, ts: u64) -> Option<&LammpsDumpFrame> {
602 self.frames.iter().find(|f| f.timestep == ts)
603 }
604
605 #[allow(dead_code)]
607 pub fn mean_position_by_type(&self, atom_type: u32) -> Option<[f64; 3]> {
608 let mut sum = [0.0f64; 3];
609 let mut count = 0usize;
610 for frame in &self.frames {
611 for pos in frame.positions_by_type(atom_type) {
612 sum[0] += pos[0];
613 sum[1] += pos[1];
614 sum[2] += pos[2];
615 count += 1;
616 }
617 }
618 if count == 0 {
619 return None;
620 }
621 Some([
622 sum[0] / count as f64,
623 sum[1] / count as f64,
624 sum[2] / count as f64,
625 ])
626 }
627}
628
629#[derive(Debug, Clone, Copy, PartialEq)]
633#[allow(dead_code)]
634pub enum LammpsAtomStyle {
635 Atomic,
637 Full,
639 Molecular,
641 Custom,
643}
644
645#[allow(dead_code)]
647pub fn detect_atom_style(header: &str) -> LammpsAtomStyle {
648 let lower = header.to_lowercase();
649 let has_q = lower.contains(" q ");
650 let has_mol = lower.contains(" mol ") || lower.contains("mol\n");
651 match (has_mol, has_q) {
652 (true, true) => LammpsAtomStyle::Full,
653 (true, false) => LammpsAtomStyle::Molecular,
654 _ => LammpsAtomStyle::Atomic,
655 }
656}
657
658#[allow(dead_code)]
668pub fn radial_distribution_function(
669 frame: &LammpsDumpFrame,
670 type_a: u32,
671 type_b: u32,
672 r_max: f64,
673 n_bins: usize,
674) -> (Vec<f64>, Vec<f64>) {
675 let dr = r_max / n_bins as f64;
676 let mut hist = vec![0u64; n_bins];
677
678 let n = frame.n_atoms;
679 let mut n_a = 0usize;
680 let mut n_b = 0usize;
681
682 for i in 0..n {
683 let ti = frame.types[i];
684 if type_a != 0 && ti != type_a {
685 continue;
686 }
687 n_a += 1;
688 for j in 0..n {
689 if i == j {
690 continue;
691 }
692 let tj = frame.types[j];
693 if type_b != 0 && tj != type_b {
694 continue;
695 }
696 n_b += 1;
697 let pi = frame.positions[i];
698 let pj = frame.positions[j];
699 let dx = pi[0] - pj[0];
700 let dy = pi[1] - pj[1];
701 let dz = pi[2] - pj[2];
702 let r = (dx * dx + dy * dy + dz * dz).sqrt();
703 if r < r_max {
704 let bin = (r / dr) as usize;
705 if bin < n_bins {
706 hist[bin] += 1;
707 }
708 }
709 }
710 }
711
712 let vol = frame.box_volume();
713 let rho = if n_b > 0 && vol > 0.0 {
714 n_b as f64 / vol
715 } else {
716 1.0
717 };
718
719 let r_bins: Vec<f64> = (0..n_bins).map(|i| (i as f64 + 0.5) * dr).collect();
720 let g_of_r: Vec<f64> = r_bins
721 .iter()
722 .zip(hist.iter())
723 .map(|(&r, &count)| {
724 let shell_vol = 4.0 * std::f64::consts::PI * r * r * dr;
725 let n_ideal = rho * shell_vol;
726 if n_ideal < 1e-30 || n_a == 0 {
727 return 0.0;
728 }
729 count as f64 / (n_a as f64 * n_ideal)
730 })
731 .collect();
732
733 (r_bins, g_of_r)
734}
735
736#[allow(dead_code)]
740pub fn extract_forces(frame: &LammpsDumpFrame) -> Option<Vec<[f64; 3]>> {
741 let fx = frame.get_custom_property("fx")?;
742 let fy = frame.get_custom_property("fy")?;
743 let fz = frame.get_custom_property("fz")?;
744 let n = frame.n_atoms;
745 let forces: Vec<[f64; 3]> = (0..n).map(|i| [fx[i], fy[i], fz[i]]).collect();
746 Some(forces)
747}
748
749#[allow(dead_code)]
751pub fn mean_force_magnitude(frame: &LammpsDumpFrame) -> Option<f64> {
752 let forces = extract_forces(frame)?;
753 if forces.is_empty() {
754 return Some(0.0);
755 }
756 let total: f64 = forces
757 .iter()
758 .map(|f| (f[0] * f[0] + f[1] * f[1] + f[2] * f[2]).sqrt())
759 .sum();
760 Some(total / forces.len() as f64)
761}
762
763#[allow(dead_code)]
774pub fn triclinic_lattice_vectors(
775 box_bounds: [[f64; 2]; 3],
776 tilts: [f64; 3],
777) -> ([f64; 3], [f64; 3], [f64; 3]) {
778 let lx = box_bounds[0][1] - box_bounds[0][0];
779 let ly = box_bounds[1][1] - box_bounds[1][0];
780 let lz = box_bounds[2][1] - box_bounds[2][0];
781 let [xy, xz, yz] = tilts;
782 let a = [lx, 0.0, 0.0];
783 let b = [xy, ly, 0.0];
784 let c = [xz, yz, lz];
785 (a, b, c)
786}
787
788#[allow(dead_code)]
790pub fn triclinic_volume(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
791 let bc = [
793 b[1] * c[2] - b[2] * c[1],
794 b[2] * c[0] - b[0] * c[2],
795 b[0] * c[1] - b[1] * c[0],
796 ];
797 a[0] * bc[0] + a[1] * bc[1] + a[2] * bc[2]
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use std::io::Cursor;
804
805 fn make_frame(timestep: u64, with_velocities: bool) -> LammpsDumpFrame {
806 let positions = vec![
807 [0.0, 0.0, 0.0],
808 [1.0, 0.0, 0.0],
809 [0.0, 1.0, 0.0],
810 [1.0, 1.0, 1.0],
811 ];
812 let velocities = if with_velocities {
813 Some(vec![
814 [0.1, 0.2, 0.3],
815 [-0.1, 0.0, 0.1],
816 [0.0, -0.2, 0.0],
817 [0.5, -0.5, 0.5],
818 ])
819 } else {
820 None
821 };
822 LammpsDumpFrame {
823 timestep,
824 n_atoms: 4,
825 box_bounds: [[0.0, 5.0], [0.0, 5.0], [0.0, 5.0]],
826 atom_ids: vec![1, 2, 3, 4],
827 types: vec![1, 1, 2, 2],
828 positions,
829 velocities,
830 tilt_factors: None,
831 custom_properties: Vec::new(),
832 }
833 }
834
835 #[test]
836 fn test_lammps_write_read_roundtrip() {
837 let frame = make_frame(100, false);
838 let mut buf: Vec<u8> = Vec::new();
839 LammpsDumpWriter::write_frame(&mut buf, &frame).expect("write failed");
840
841 let parsed = LammpsDumpReader::read_frame(Cursor::new(&buf))
842 .expect("no frame found")
843 .expect("parse error");
844
845 assert_eq!(parsed.timestep, 100);
846 assert_eq!(parsed.n_atoms, 4);
847 assert_eq!(parsed.atom_ids, vec![1, 2, 3, 4]);
848 assert_eq!(parsed.types, vec![1, 1, 2, 2]);
849 assert!((parsed.positions[0][0]).abs() < 1e-10);
850 assert!((parsed.positions[1][0] - 1.0).abs() < 1e-10);
851 assert!((parsed.positions[3][2] - 1.0).abs() < 1e-10);
852 assert!(parsed.velocities.is_none());
853 }
854
855 #[test]
856 fn test_lammps_multi_frame() {
857 let mut buf: Vec<u8> = Vec::new();
858 for ts in [0u64, 100, 200] {
859 let frame = make_frame(ts, false);
860 LammpsDumpWriter::write_frame(&mut buf, &frame).expect("write failed");
861 }
862
863 let text = String::from_utf8(buf).expect("utf8");
865 let mut timesteps_found = Vec::new();
866
867 let frames_text: Vec<&str> = text
869 .split("ITEM: TIMESTEP\n")
870 .filter(|s| !s.trim().is_empty())
871 .collect();
872 assert_eq!(frames_text.len(), 3, "expected 3 frames");
873
874 for chunk in &frames_text {
875 let full = format!("ITEM: TIMESTEP\n{chunk}");
876 let result = LammpsDumpReader::read_frame(Cursor::new(full.as_bytes()))
877 .expect("no frame")
878 .expect("parse error");
879 timesteps_found.push(result.timestep);
880 }
881
882 assert_eq!(timesteps_found, vec![0, 100, 200]);
883 }
884
885 #[test]
886 fn test_lammps_box_bounds() {
887 let frame = LammpsDumpFrame {
888 timestep: 0,
889 n_atoms: 1,
890 box_bounds: [[-2.5, 2.5], [0.0, 10.0], [1.0, 9.0]],
891 atom_ids: vec![1],
892 types: vec![1],
893 positions: vec![[0.0, 5.0, 5.0]],
894 velocities: None,
895 tilt_factors: None,
896 custom_properties: Vec::new(),
897 };
898 let mut buf: Vec<u8> = Vec::new();
899 LammpsDumpWriter::write_frame(&mut buf, &frame).expect("write");
900
901 let parsed = LammpsDumpReader::read_frame(Cursor::new(&buf))
902 .expect("no frame")
903 .expect("parse");
904
905 assert!((parsed.box_bounds[0][0] - (-2.5)).abs() < 1e-10);
906 assert!((parsed.box_bounds[0][1] - 2.5).abs() < 1e-10);
907 assert!((parsed.box_bounds[1][0]).abs() < 1e-10);
908 assert!((parsed.box_bounds[1][1] - 10.0).abs() < 1e-10);
909 assert!((parsed.box_bounds[2][0] - 1.0).abs() < 1e-10);
910 assert!((parsed.box_bounds[2][1] - 9.0).abs() < 1e-10);
911 }
912
913 #[test]
914 fn test_lammps_with_velocities() {
915 let frame = make_frame(50, true);
916 let mut buf: Vec<u8> = Vec::new();
917 LammpsDumpWriter::write_frame(&mut buf, &frame).expect("write");
918
919 let parsed = LammpsDumpReader::read_frame(Cursor::new(&buf))
920 .expect("no frame")
921 .expect("parse");
922
923 assert_eq!(parsed.timestep, 50);
924 let vels = parsed.velocities.expect("velocities missing");
925 assert_eq!(vels.len(), 4);
926 assert!((vels[0][0] - 0.1).abs() < 1e-10);
927 assert!((vels[0][1] - 0.2).abs() < 1e-10);
928 assert!((vels[0][2] - 0.3).abs() < 1e-10);
929 assert!((vels[1][0] - (-0.1)).abs() < 1e-10);
930 assert!((vels[3][0] - 0.5).abs() < 1e-10);
931 assert!((vels[3][2] - 0.5).abs() < 1e-10);
932 }
933
934 #[test]
937 fn test_positions_by_type() {
938 let frame = make_frame(0, false);
939 let type1 = frame.positions_by_type(1);
940 assert_eq!(type1.len(), 2);
941 let type2 = frame.positions_by_type(2);
942 assert_eq!(type2.len(), 2);
943 let type3 = frame.positions_by_type(3);
944 assert!(type3.is_empty());
945 }
946
947 #[test]
948 fn test_count_by_type() {
949 let frame = make_frame(0, false);
950 assert_eq!(frame.count_by_type(1), 2);
951 assert_eq!(frame.count_by_type(2), 2);
952 assert_eq!(frame.count_by_type(99), 0);
953 }
954
955 #[test]
956 fn test_box_dimensions() {
957 let frame = make_frame(0, false);
958 let dims = frame.box_dimensions();
959 assert!((dims[0] - 5.0).abs() < 1e-12);
960 assert!((dims[1] - 5.0).abs() < 1e-12);
961 assert!((dims[2] - 5.0).abs() < 1e-12);
962 }
963
964 #[test]
965 fn test_box_center() {
966 let frame = make_frame(0, false);
967 let center = frame.box_center();
968 assert!((center[0] - 2.5).abs() < 1e-12);
969 assert!((center[1] - 2.5).abs() < 1e-12);
970 assert!((center[2] - 2.5).abs() < 1e-12);
971 }
972
973 #[test]
974 fn test_box_volume() {
975 let frame = make_frame(0, false);
976 assert!((frame.box_volume() - 125.0).abs() < 1e-10);
977 }
978
979 #[test]
980 fn test_center_of_mass() {
981 let frame = make_frame(0, false);
982 let com = frame.center_of_mass();
983 assert!((com[0] - 0.5).abs() < 1e-12);
986 assert!((com[1] - 0.5).abs() < 1e-12);
987 assert!((com[2] - 0.25).abs() < 1e-12);
988 }
989
990 #[test]
991 fn test_center_of_mass_empty() {
992 let frame = LammpsDumpFrame {
993 timestep: 0,
994 n_atoms: 0,
995 box_bounds: [[0.0, 1.0]; 3],
996 atom_ids: vec![],
997 types: vec![],
998 positions: vec![],
999 velocities: None,
1000 tilt_factors: None,
1001 custom_properties: Vec::new(),
1002 };
1003 let com = frame.center_of_mass();
1004 assert_eq!(com, [0.0, 0.0, 0.0]);
1005 }
1006
1007 #[test]
1008 fn test_atom_types() {
1009 let frame = make_frame(0, false);
1010 let types = frame.atom_types();
1011 assert_eq!(types, vec![1, 2]);
1012 }
1013
1014 #[test]
1015 fn test_read_all_frames() {
1016 let mut buf: Vec<u8> = Vec::new();
1017 for ts in [0u64, 100, 200] {
1018 LammpsDumpWriter::write_frame(&mut buf, &make_frame(ts, false)).unwrap();
1019 }
1020 let text = String::from_utf8(buf).unwrap();
1021 let frames = LammpsDumpReader::read_all_frames(&text).unwrap();
1022 assert_eq!(frames.len(), 3);
1023 assert_eq!(frames[0].timestep, 0);
1024 assert_eq!(frames[1].timestep, 100);
1025 assert_eq!(frames[2].timestep, 200);
1026 }
1027
1028 #[test]
1029 fn test_extract_timesteps() {
1030 let mut buf: Vec<u8> = Vec::new();
1031 for ts in [10u64, 20, 30] {
1032 LammpsDumpWriter::write_frame(&mut buf, &make_frame(ts, false)).unwrap();
1033 }
1034 let text = String::from_utf8(buf).unwrap();
1035 let timesteps = LammpsDumpReader::extract_timesteps(&text);
1036 assert_eq!(timesteps, vec![10, 20, 30]);
1037 }
1038
1039 #[test]
1040 fn test_triclinic_write_read() {
1041 let frame = LammpsDumpFrame {
1042 timestep: 42,
1043 n_atoms: 2,
1044 box_bounds: [[0.0, 10.0], [0.0, 10.0], [0.0, 10.0]],
1045 atom_ids: vec![1, 2],
1046 types: vec![1, 1],
1047 positions: vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
1048 velocities: None,
1049 tilt_factors: Some([0.5, 0.0, -0.3]),
1050 custom_properties: Vec::new(),
1051 };
1052 let mut buf: Vec<u8> = Vec::new();
1053 LammpsDumpWriter::write_frame(&mut buf, &frame).unwrap();
1054
1055 let parsed = LammpsDumpReader::read_frame(Cursor::new(&buf))
1056 .unwrap()
1057 .unwrap();
1058
1059 assert_eq!(parsed.timestep, 42);
1060 let tilts = parsed.tilt_factors.expect("tilt factors missing");
1061 assert!((tilts[0] - 0.5).abs() < 1e-10);
1062 assert!((tilts[1] - 0.0).abs() < 1e-10);
1063 assert!((tilts[2] - (-0.3)).abs() < 1e-10);
1064 }
1065
1066 #[test]
1067 fn test_number_density() {
1068 let frame = make_frame(0, false);
1069 let rho = number_density(&frame);
1070 assert!((rho - 0.032).abs() < 1e-10);
1072 }
1073
1074 #[test]
1075 fn test_mean_kinetic_energy() {
1076 let frame = make_frame(0, true);
1077 let ke = mean_kinetic_energy(&frame).unwrap();
1078 assert!((ke - 0.11875).abs() < 1e-10);
1083 }
1084
1085 #[test]
1086 fn test_mean_kinetic_energy_no_velocities() {
1087 let frame = make_frame(0, false);
1088 assert!(mean_kinetic_energy(&frame).is_none());
1089 }
1090
1091 #[test]
1092 fn test_write_frames_multiple() {
1093 let frames: Vec<LammpsDumpFrame> = (0..3).map(|i| make_frame(i * 50, false)).collect();
1094 let mut buf: Vec<u8> = Vec::new();
1095 LammpsDumpWriter::write_frames(&mut buf, &frames).unwrap();
1096 let text = String::from_utf8(buf).unwrap();
1097 let timesteps = LammpsDumpReader::extract_timesteps(&text);
1098 assert_eq!(timesteps, vec![0, 50, 100]);
1099 }
1100
1101 #[test]
1102 fn test_custom_properties_roundtrip() {
1103 let frame = LammpsDumpFrame {
1104 timestep: 0,
1105 n_atoms: 2,
1106 box_bounds: [[0.0, 5.0]; 3],
1107 atom_ids: vec![1, 2],
1108 types: vec![1, 1],
1109 positions: vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
1110 velocities: None,
1111 tilt_factors: None,
1112 custom_properties: vec![("energy".to_string(), vec![10.5, 20.3])],
1113 };
1114 let mut buf: Vec<u8> = Vec::new();
1115 LammpsDumpWriter::write_frame(&mut buf, &frame).unwrap();
1116 let text = String::from_utf8(buf).unwrap();
1117 assert!(text.contains("energy"));
1119 }
1120
1121 #[test]
1122 fn test_get_custom_property() {
1123 let frame = LammpsDumpFrame {
1124 timestep: 0,
1125 n_atoms: 2,
1126 box_bounds: [[0.0, 1.0]; 3],
1127 atom_ids: vec![1, 2],
1128 types: vec![1, 1],
1129 positions: vec![[0.0; 3]; 2],
1130 velocities: None,
1131 tilt_factors: None,
1132 custom_properties: vec![("charge".to_string(), vec![1.0, -1.0])],
1133 };
1134 let charge = frame.get_custom_property("charge").unwrap();
1135 assert_eq!(charge.len(), 2);
1136 assert!((charge[0] - 1.0).abs() < 1e-12);
1137 assert!((charge[1] - (-1.0)).abs() < 1e-12);
1138 assert!(frame.get_custom_property("missing").is_none());
1139 }
1140
1141 #[test]
1142 fn test_box_dimensions_asymmetric() {
1143 let frame = LammpsDumpFrame {
1144 timestep: 0,
1145 n_atoms: 0,
1146 box_bounds: [[-5.0, 5.0], [0.0, 20.0], [10.0, 30.0]],
1147 atom_ids: vec![],
1148 types: vec![],
1149 positions: vec![],
1150 velocities: None,
1151 tilt_factors: None,
1152 custom_properties: Vec::new(),
1153 };
1154 let dims = frame.box_dimensions();
1155 assert!((dims[0] - 10.0).abs() < 1e-12);
1156 assert!((dims[1] - 20.0).abs() < 1e-12);
1157 assert!((dims[2] - 20.0).abs() < 1e-12);
1158 }
1159
1160 fn make_frame_with_stress(timestep: u64) -> LammpsDumpFrame {
1163 LammpsDumpFrame {
1164 timestep,
1165 n_atoms: 2,
1166 box_bounds: [[0.0, 10.0]; 3],
1167 atom_ids: vec![1, 2],
1168 types: vec![1, 1],
1169 positions: vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
1170 velocities: None,
1171 tilt_factors: None,
1172 custom_properties: vec![
1173 ("sxx".to_string(), vec![1.0, 2.0]),
1174 ("syy".to_string(), vec![3.0, 4.0]),
1175 ("szz".to_string(), vec![5.0, 6.0]),
1176 ("sxy".to_string(), vec![0.5, 0.5]),
1177 ("sxz".to_string(), vec![0.2, 0.2]),
1178 ("syz".to_string(), vec![0.1, 0.1]),
1179 ],
1180 }
1181 }
1182
1183 #[test]
1184 fn test_extract_stress_tensor_present() {
1185 let frame = make_frame_with_stress(0);
1186 let tensors = extract_stress_tensor(&frame).expect("stress tensors should be present");
1187 assert_eq!(tensors.len(), 2);
1188 assert!((tensors[0][0] - 1.0).abs() < 1e-12); assert!((tensors[1][0] - 2.0).abs() < 1e-12); assert!((tensors[0][2] - 5.0).abs() < 1e-12); }
1192
1193 #[test]
1194 fn test_extract_stress_tensor_missing() {
1195 let frame = make_frame(0, false);
1196 assert!(extract_stress_tensor(&frame).is_none());
1197 }
1198
1199 #[test]
1200 fn test_von_mises_stress_isotropic() {
1201 let s = [5.0, 5.0, 5.0, 0.0, 0.0, 0.0];
1203 let vm = von_mises_stress(s);
1204 assert!(vm.abs() < 1e-10);
1205 }
1206
1207 #[test]
1208 fn test_von_mises_stress_uniaxial() {
1209 let s = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1211 let vm = von_mises_stress(s);
1212 assert!((vm - 1.0).abs() < 1e-10);
1213 }
1214
1215 #[test]
1216 fn test_hydrostatic_pressure() {
1217 let s = [3.0, 3.0, 3.0, 0.0, 0.0, 0.0];
1218 let p = hydrostatic_pressure(s);
1219 assert!((p - (-3.0)).abs() < 1e-12);
1220 }
1221
1222 #[test]
1223 fn test_hydrostatic_pressure_compression() {
1224 let s = [-10.0, -10.0, -10.0, 0.0, 0.0, 0.0];
1226 let p = hydrostatic_pressure(s);
1227 assert!((p - 10.0).abs() < 1e-12);
1228 }
1229
1230 #[test]
1231 fn test_dump_time_series_new_len() {
1232 let frames: Vec<LammpsDumpFrame> = (0..5).map(|i| make_frame(i * 100, false)).collect();
1233 let ts = DumpTimeSeries::new(frames);
1234 assert_eq!(ts.len(), 5);
1235 assert!(!ts.is_empty());
1236 }
1237
1238 #[test]
1239 fn test_dump_time_series_empty() {
1240 let ts = DumpTimeSeries::new(vec![]);
1241 assert_eq!(ts.len(), 0);
1242 assert!(ts.is_empty());
1243 }
1244
1245 #[test]
1246 fn test_dump_time_series_timesteps() {
1247 let frames: Vec<LammpsDumpFrame> = (0..3).map(|i| make_frame(i * 50, false)).collect();
1248 let ts = DumpTimeSeries::new(frames);
1249 assert_eq!(ts.timesteps(), vec![0, 50, 100]);
1250 }
1251
1252 #[test]
1253 fn test_dump_time_series_from_str() {
1254 let mut buf: Vec<u8> = Vec::new();
1255 for i in 0..3u64 {
1256 LammpsDumpWriter::write_frame(&mut buf, &make_frame(i * 10, false)).unwrap();
1257 }
1258 let content = String::from_utf8(buf).unwrap();
1259 let ts = DumpTimeSeries::from_str(&content).unwrap();
1260 assert_eq!(ts.len(), 3);
1261 assert_eq!(ts.timesteps(), vec![0, 10, 20]);
1262 }
1263
1264 #[test]
1265 fn test_dump_time_series_mean_ke_series() {
1266 let frames: Vec<LammpsDumpFrame> = vec![
1267 make_frame(0, true),
1268 make_frame(1, false),
1269 make_frame(2, true),
1270 ];
1271 let ts = DumpTimeSeries::new(frames);
1272 let ke_series = ts.mean_ke_series();
1273 assert_eq!(ke_series.len(), 3);
1274 assert!(ke_series[0].is_some());
1275 assert!(ke_series[1].is_none());
1276 assert!(ke_series[2].is_some());
1277 }
1278
1279 #[test]
1280 fn test_dump_time_series_number_density_series() {
1281 let frames: Vec<LammpsDumpFrame> = (0..3).map(|i| make_frame(i, false)).collect();
1282 let ts = DumpTimeSeries::new(frames);
1283 let rho_series = ts.number_density_series();
1284 assert_eq!(rho_series.len(), 3);
1286 for rho in &rho_series {
1287 assert!((rho - 0.032).abs() < 1e-10);
1288 }
1289 }
1290
1291 #[test]
1292 fn test_dump_time_series_frame_at_timestep() {
1293 let frames: Vec<LammpsDumpFrame> = (0..5).map(|i| make_frame(i * 100, false)).collect();
1294 let ts = DumpTimeSeries::new(frames);
1295 assert!(ts.frame_at_timestep(200).is_some());
1296 assert_eq!(ts.frame_at_timestep(200).unwrap().timestep, 200);
1297 assert!(ts.frame_at_timestep(999).is_none());
1298 }
1299
1300 #[test]
1301 fn test_dump_time_series_mean_position_by_type() {
1302 let frames: Vec<LammpsDumpFrame> = (0..2).map(|i| make_frame(i, false)).collect();
1303 let ts = DumpTimeSeries::new(frames);
1304 let mean = ts
1307 .mean_position_by_type(1)
1308 .expect("type 1 should have positions");
1309 assert!((mean[0] - 0.5).abs() < 1e-12);
1310 assert!((mean[1] - 0.0).abs() < 1e-12);
1311 assert!((mean[2] - 0.0).abs() < 1e-12);
1312 assert!(ts.mean_position_by_type(99).is_none());
1313 }
1314
1315 #[test]
1316 fn test_detect_atom_style_atomic() {
1317 let style = detect_atom_style("ITEM: ATOMS id type x y z");
1318 assert_eq!(style, LammpsAtomStyle::Atomic);
1319 }
1320
1321 #[test]
1322 fn test_detect_atom_style_full() {
1323 let style = detect_atom_style("ITEM: ATOMS id mol type q x y z");
1324 assert_eq!(style, LammpsAtomStyle::Full);
1325 }
1326
1327 #[test]
1328 fn test_detect_atom_style_molecular() {
1329 let style = detect_atom_style("ITEM: ATOMS id mol type x y z");
1330 assert_eq!(style, LammpsAtomStyle::Molecular);
1331 }
1332
1333 #[test]
1334 fn test_rdf_length() {
1335 let frame = make_frame(0, false);
1336 let n_bins = 20;
1337 let (r_bins, g) = radial_distribution_function(&frame, 0, 0, 2.5, n_bins);
1338 assert_eq!(r_bins.len(), n_bins);
1339 assert_eq!(g.len(), n_bins);
1340 }
1341
1342 #[test]
1343 fn test_rdf_r_bins_positive() {
1344 let frame = make_frame(0, false);
1345 let (r_bins, _) = radial_distribution_function(&frame, 0, 0, 2.0, 10);
1346 for &r in &r_bins {
1347 assert!(r > 0.0);
1348 }
1349 }
1350
1351 #[test]
1352 fn test_rdf_nonnegative() {
1353 let frame = make_frame(0, false);
1354 let (_, g) = radial_distribution_function(&frame, 0, 0, 2.5, 20);
1355 for &v in &g {
1356 assert!(v >= 0.0);
1357 }
1358 }
1359
1360 #[test]
1361 fn test_extract_forces_present() {
1362 let frame = LammpsDumpFrame {
1363 timestep: 0,
1364 n_atoms: 2,
1365 box_bounds: [[0.0, 5.0]; 3],
1366 atom_ids: vec![1, 2],
1367 types: vec![1, 1],
1368 positions: vec![[0.0; 3]; 2],
1369 velocities: None,
1370 tilt_factors: None,
1371 custom_properties: vec![
1372 ("fx".to_string(), vec![1.0, -1.0]),
1373 ("fy".to_string(), vec![0.5, -0.5]),
1374 ("fz".to_string(), vec![0.0, 0.0]),
1375 ],
1376 };
1377 let forces = extract_forces(&frame).expect("forces should be present");
1378 assert_eq!(forces.len(), 2);
1379 assert!((forces[0][0] - 1.0).abs() < 1e-12);
1380 assert!((forces[1][0] - (-1.0)).abs() < 1e-12);
1381 }
1382
1383 #[test]
1384 fn test_extract_forces_missing() {
1385 let frame = make_frame(0, false);
1386 assert!(extract_forces(&frame).is_none());
1387 }
1388
1389 #[test]
1390 fn test_mean_force_magnitude() {
1391 let frame = LammpsDumpFrame {
1392 timestep: 0,
1393 n_atoms: 1,
1394 box_bounds: [[0.0, 5.0]; 3],
1395 atom_ids: vec![1],
1396 types: vec![1],
1397 positions: vec![[0.0; 3]],
1398 velocities: None,
1399 tilt_factors: None,
1400 custom_properties: vec![
1401 ("fx".to_string(), vec![3.0]),
1402 ("fy".to_string(), vec![4.0]),
1403 ("fz".to_string(), vec![0.0]),
1404 ],
1405 };
1406 let mean_f = mean_force_magnitude(&frame).expect("should have forces");
1407 assert!((mean_f - 5.0).abs() < 1e-10);
1408 }
1409
1410 #[test]
1411 fn test_triclinic_lattice_vectors_orthogonal() {
1412 let bounds = [[0.0, 10.0], [0.0, 8.0], [0.0, 6.0]];
1413 let tilts = [0.0, 0.0, 0.0];
1414 let (a, b, c) = triclinic_lattice_vectors(bounds, tilts);
1415 assert!((a[0] - 10.0).abs() < 1e-12);
1416 assert!((a[1]).abs() < 1e-12);
1417 assert!((b[0]).abs() < 1e-12);
1418 assert!((b[1] - 8.0).abs() < 1e-12);
1419 assert!((c[2] - 6.0).abs() < 1e-12);
1420 }
1421
1422 #[test]
1423 fn test_triclinic_lattice_vectors_tilted() {
1424 let bounds = [[0.0, 10.0], [0.0, 10.0], [0.0, 10.0]];
1425 let tilts = [2.0, 1.0, 0.5];
1426 let (a, b, c) = triclinic_lattice_vectors(bounds, tilts);
1427 assert!((a[0] - 10.0).abs() < 1e-12);
1428 assert!((b[0] - 2.0).abs() < 1e-12); assert!((c[0] - 1.0).abs() < 1e-12); assert!((c[1] - 0.5).abs() < 1e-12); }
1432
1433 #[test]
1434 fn test_triclinic_volume_orthogonal() {
1435 let a = [10.0, 0.0, 0.0];
1436 let b = [0.0, 8.0, 0.0];
1437 let c = [0.0, 0.0, 6.0];
1438 let vol = triclinic_volume(a, b, c);
1439 assert!((vol - 480.0).abs() < 1e-10);
1440 }
1441
1442 #[test]
1443 fn test_triclinic_volume_tilted() {
1444 let bounds = [[0.0, 10.0]; 3];
1446 let tilts = [1.0, 0.0, 0.0];
1447 let (a, b, c) = triclinic_lattice_vectors(bounds, tilts);
1448 let vol = triclinic_volume(a, b, c);
1449 assert!((vol - 1000.0).abs() < 1e-8);
1451 }
1452}