1#![allow(clippy::needless_range_loop)]
2use std::collections::HashMap;
18use std::fmt;
19use std::io::{BufRead, BufReader, BufWriter, Read, Write};
20#[derive(Debug)]
26pub enum AmberIoError {
27 ParseError(String),
29 IoError(String),
31 MissingSection(String),
33 UnexpectedEof,
35}
36
37impl fmt::Display for AmberIoError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 AmberIoError::ParseError(s) => write!(f, "AMBER parse error: {s}"),
41 AmberIoError::IoError(s) => write!(f, "AMBER I/O error: {s}"),
42 AmberIoError::MissingSection(s) => write!(f, "AMBER missing section: {s}"),
43 AmberIoError::UnexpectedEof => write!(f, "AMBER unexpected end of file"),
44 }
45 }
46}
47
48impl std::error::Error for AmberIoError {}
49
50#[derive(Debug, Clone, PartialEq)]
56pub struct PrmtopAtom {
57 pub name: String,
59 pub charge: f64,
61 pub mass: f64,
63 pub atom_type: String,
65}
66
67impl PrmtopAtom {
68 pub fn new(
70 name: impl Into<String>,
71 charge: f64,
72 mass: f64,
73 atom_type: impl Into<String>,
74 ) -> Self {
75 PrmtopAtom {
76 name: name.into(),
77 charge,
78 mass,
79 atom_type: atom_type.into(),
80 }
81 }
82
83 pub fn charge_si(&self) -> f64 {
85 self.charge / 18.2223
86 }
87}
88
89#[derive(Debug, Clone, PartialEq)]
95pub struct PrmtopBond {
96 pub atom_i: usize,
98 pub atom_j: usize,
100 pub force_constant: f64,
102 pub eq_length: f64,
104}
105
106impl PrmtopBond {
107 pub fn new(atom_i: usize, atom_j: usize, force_constant: f64, eq_length: f64) -> Self {
109 PrmtopBond {
110 atom_i,
111 atom_j,
112 force_constant,
113 eq_length,
114 }
115 }
116
117 pub fn energy(&self, r: f64) -> f64 {
121 self.force_constant * (r - self.eq_length).powi(2)
122 }
123}
124
125#[derive(Debug, Clone, PartialEq)]
131pub struct PrmtopAngle {
132 pub atom_i: usize,
134 pub atom_j: usize,
136 pub atom_k: usize,
138 pub force_constant: f64,
140 pub eq_angle: f64,
142}
143
144impl PrmtopAngle {
145 pub fn new(
147 atom_i: usize,
148 atom_j: usize,
149 atom_k: usize,
150 force_constant: f64,
151 eq_angle: f64,
152 ) -> Self {
153 PrmtopAngle {
154 atom_i,
155 atom_j,
156 atom_k,
157 force_constant,
158 eq_angle,
159 }
160 }
161
162 pub fn energy(&self, theta: f64) -> f64 {
166 self.force_constant * (theta - self.eq_angle).powi(2)
167 }
168}
169
170#[derive(Debug, Clone, Default)]
179pub struct PrmtopFile {
180 pub version: String,
182 pub atoms: Vec<PrmtopAtom>,
184 pub bonds: Vec<PrmtopBond>,
186 pub angles: Vec<PrmtopAngle>,
188 pub extra_sections: HashMap<String, Vec<String>>,
190}
191
192impl PrmtopFile {
193 pub fn new() -> Self {
195 PrmtopFile::default()
196 }
197
198 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), AmberIoError> {
204 let mut bw = BufWriter::new(writer);
205 writeln!(bw, "%VERSION {}", self.version)
206 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
207 writeln!(bw, "%FLAG ATOM_NAME").map_err(|e| AmberIoError::IoError(e.to_string()))?;
208 writeln!(bw, "%FORMAT(20a4)").map_err(|e| AmberIoError::IoError(e.to_string()))?;
209 for (i, atom) in self.atoms.iter().enumerate() {
210 write!(bw, "{:<4}", &atom.name).map_err(|e| AmberIoError::IoError(e.to_string()))?;
211 if (i + 1) % 20 == 0 {
212 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
213 }
214 }
215 if !self.atoms.len().is_multiple_of(20) {
216 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
217 }
218
219 writeln!(bw, "%FLAG CHARGE").map_err(|e| AmberIoError::IoError(e.to_string()))?;
220 writeln!(bw, "%FORMAT(5E16.8)").map_err(|e| AmberIoError::IoError(e.to_string()))?;
221 for (i, atom) in self.atoms.iter().enumerate() {
222 write!(bw, "{:16.8E}", atom.charge)
223 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
224 if (i + 1) % 5 == 0 {
225 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
226 }
227 }
228 if !self.atoms.len().is_multiple_of(5) {
229 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
230 }
231
232 writeln!(bw, "%FLAG MASS").map_err(|e| AmberIoError::IoError(e.to_string()))?;
233 writeln!(bw, "%FORMAT(5E16.8)").map_err(|e| AmberIoError::IoError(e.to_string()))?;
234 for (i, atom) in self.atoms.iter().enumerate() {
235 write!(bw, "{:16.8E}", atom.mass).map_err(|e| AmberIoError::IoError(e.to_string()))?;
236 if (i + 1) % 5 == 0 {
237 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
238 }
239 }
240 if !self.atoms.len().is_multiple_of(5) {
241 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
242 }
243
244 writeln!(bw, "%FLAG ATOM_TYPE_INDEX").map_err(|e| AmberIoError::IoError(e.to_string()))?;
245 writeln!(bw, "%FORMAT(20a4)").map_err(|e| AmberIoError::IoError(e.to_string()))?;
246 for (i, atom) in self.atoms.iter().enumerate() {
247 write!(bw, "{:<4}", &atom.atom_type)
248 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
249 if (i + 1) % 20 == 0 {
250 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
251 }
252 }
253 if !self.atoms.len().is_multiple_of(20) {
254 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
255 }
256
257 writeln!(bw, "%FLAG BOND_FORCE_CONSTANT")
259 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
260 writeln!(bw, "%FORMAT(5E16.8)").map_err(|e| AmberIoError::IoError(e.to_string()))?;
261 for (i, bond) in self.bonds.iter().enumerate() {
262 write!(bw, "{:16.8E}", bond.force_constant)
263 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
264 if (i + 1) % 5 == 0 {
265 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
266 }
267 }
268 if !self.bonds.is_empty() && !self.bonds.len().is_multiple_of(5) {
269 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
270 }
271
272 writeln!(bw, "%FLAG BOND_EQUIL_VALUE").map_err(|e| AmberIoError::IoError(e.to_string()))?;
273 writeln!(bw, "%FORMAT(5E16.8)").map_err(|e| AmberIoError::IoError(e.to_string()))?;
274 for (i, bond) in self.bonds.iter().enumerate() {
275 write!(bw, "{:16.8E}", bond.eq_length)
276 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
277 if (i + 1) % 5 == 0 {
278 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
279 }
280 }
281 if !self.bonds.is_empty() && !self.bonds.len().is_multiple_of(5) {
282 writeln!(bw).map_err(|e| AmberIoError::IoError(e.to_string()))?;
283 }
284
285 writeln!(bw, "%END").map_err(|e| AmberIoError::IoError(e.to_string()))?;
286 Ok(())
287 }
288
289 pub fn read<R: Read>(reader: R) -> Result<Self, AmberIoError> {
291 let br = BufReader::new(reader);
292 let mut file = PrmtopFile::new();
293 let lines: Vec<String> = br
294 .lines()
295 .map(|l| l.map_err(|e| AmberIoError::IoError(e.to_string())))
296 .collect::<Result<_, _>>()?;
297
298 let mut i = 0usize;
299 let mut names: Vec<String> = Vec::new();
300 let mut charges: Vec<f64> = Vec::new();
301 let mut masses: Vec<f64> = Vec::new();
302 let mut types: Vec<String> = Vec::new();
303 let mut bond_kf: Vec<f64> = Vec::new();
304 let mut bond_eq: Vec<f64> = Vec::new();
305
306 while i < lines.len() {
307 let line = lines[i].trim();
308 if line.starts_with("%VERSION") {
309 file.version = line.trim_start_matches("%VERSION").trim().to_string();
310 } else if line == "%FLAG ATOM_NAME" {
311 i += 1; i += 1;
313 while i < lines.len() && !lines[i].starts_with('%') {
314 let row = &lines[i];
315 let mut pos = 0usize;
316 while pos + 4 <= row.len() {
317 names.push(row[pos..pos + 4].trim().to_string());
318 pos += 4;
319 }
320 i += 1;
321 }
322 continue;
323 } else if line == "%FLAG CHARGE" {
324 i += 1;
325 i += 1;
326 while i < lines.len() && !lines[i].starts_with('%') {
327 for tok in lines[i].split_whitespace() {
328 charges.push(
329 tok.parse::<f64>()
330 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
331 );
332 }
333 i += 1;
334 }
335 continue;
336 } else if line == "%FLAG MASS" {
337 i += 1;
338 i += 1;
339 while i < lines.len() && !lines[i].starts_with('%') {
340 for tok in lines[i].split_whitespace() {
341 masses.push(
342 tok.parse::<f64>()
343 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
344 );
345 }
346 i += 1;
347 }
348 continue;
349 } else if line == "%FLAG ATOM_TYPE_INDEX" {
350 i += 1;
351 i += 1;
352 while i < lines.len() && !lines[i].starts_with('%') {
353 let row = &lines[i];
354 let mut pos = 0usize;
355 while pos + 4 <= row.len() {
356 types.push(row[pos..pos + 4].trim().to_string());
357 pos += 4;
358 }
359 i += 1;
360 }
361 continue;
362 } else if line == "%FLAG BOND_FORCE_CONSTANT" {
363 i += 1;
364 i += 1;
365 while i < lines.len() && !lines[i].starts_with('%') {
366 for tok in lines[i].split_whitespace() {
367 bond_kf.push(
368 tok.parse::<f64>()
369 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
370 );
371 }
372 i += 1;
373 }
374 continue;
375 } else if line == "%FLAG BOND_EQUIL_VALUE" {
376 i += 1;
377 i += 1;
378 while i < lines.len() && !lines[i].starts_with('%') {
379 for tok in lines[i].split_whitespace() {
380 bond_eq.push(
381 tok.parse::<f64>()
382 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
383 );
384 }
385 i += 1;
386 }
387 continue;
388 }
389 i += 1;
390 }
391
392 let n = names.len();
393 for idx in 0..n {
394 file.atoms.push(PrmtopAtom {
395 name: names.get(idx).cloned().unwrap_or_default(),
396 charge: charges.get(idx).copied().unwrap_or(0.0),
397 mass: masses.get(idx).copied().unwrap_or(0.0),
398 atom_type: types.get(idx).cloned().unwrap_or_default(),
399 });
400 }
401
402 for idx in 0..bond_kf.len() {
403 file.bonds.push(PrmtopBond {
404 atom_i: idx * 2,
405 atom_j: idx * 2 + 1,
406 force_constant: bond_kf[idx],
407 eq_length: bond_eq.get(idx).copied().unwrap_or(0.0),
408 });
409 }
410
411 Ok(file)
412 }
413
414 pub fn natom(&self) -> usize {
416 self.atoms.len()
417 }
418}
419
420#[derive(Debug, Clone, Default)]
435pub struct Rst7File {
436 pub title: String,
438 pub time: Option<f64>,
440 pub coordinates: Vec<f64>,
442 pub velocities: Vec<f64>,
444 pub box_dimensions: Option<[f64; 6]>,
446}
447
448impl Rst7File {
449 pub fn new() -> Self {
451 Rst7File::default()
452 }
453
454 pub fn natom(&self) -> usize {
456 self.coordinates.len() / 3
457 }
458
459 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), AmberIoError> {
464 let mut bw = BufWriter::new(writer);
465 writeln!(bw, "{}", self.title).map_err(|e| AmberIoError::IoError(e.to_string()))?;
466 if let Some(t) = self.time {
467 writeln!(bw, "{:6}{:15.7e}", self.natom(), t)
468 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
469 } else {
470 writeln!(bw, "{:6}", self.natom()).map_err(|e| AmberIoError::IoError(e.to_string()))?;
471 }
472
473 write_f64_block(&mut bw, &self.coordinates, 6)?;
474
475 if !self.velocities.is_empty() {
476 write_f64_block(&mut bw, &self.velocities, 6)?;
477 }
478
479 if let Some(b) = self.box_dimensions {
480 writeln!(
481 bw,
482 "{:12.7}{:12.7}{:12.7}{:12.7}{:12.7}{:12.7}",
483 b[0], b[1], b[2], b[3], b[4], b[5]
484 )
485 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
486 }
487
488 Ok(())
489 }
490
491 pub fn read<R: Read>(reader: R) -> Result<Self, AmberIoError> {
493 let br = BufReader::new(reader);
494 let mut lines = br.lines();
495 let mut rst = Rst7File::new();
496
497 rst.title = lines
499 .next()
500 .ok_or(AmberIoError::UnexpectedEof)?
501 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
502
503 let header = lines
505 .next()
506 .ok_or(AmberIoError::UnexpectedEof)?
507 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
508 let mut hparts = header.split_whitespace();
509 let natom: usize = hparts
510 .next()
511 .ok_or(AmberIoError::ParseError("missing natom".into()))?
512 .parse()
513 .map_err(|e: std::num::ParseIntError| AmberIoError::ParseError(e.to_string()))?;
514 if let Some(t) = hparts.next() {
515 rst.time = Some(
516 t.parse::<f64>()
517 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
518 );
519 }
520
521 let n_coord = natom * 3;
522 let mut all_values: Vec<f64> = Vec::new();
523
524 for line in lines {
525 let l = line.map_err(|e| AmberIoError::IoError(e.to_string()))?;
526 let trimmed = l.trim();
527 if trimmed.is_empty() {
528 continue;
529 }
530 let parts: Vec<&str> = trimmed.split_whitespace().collect();
532 if all_values.len() >= n_coord * 2 && parts.len() == 6 {
534 let mut box_vals = [0f64; 6];
535 for (i, p) in parts.iter().enumerate() {
536 box_vals[i] = p
537 .parse::<f64>()
538 .map_err(|e| AmberIoError::ParseError(e.to_string()))?;
539 }
540 rst.box_dimensions = Some(box_vals);
541 break;
542 }
543 for p in parts {
544 all_values.push(
545 p.parse::<f64>()
546 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
547 );
548 }
549 }
550
551 rst.coordinates = all_values[..n_coord.min(all_values.len())].to_vec();
552 if all_values.len() > n_coord {
553 rst.velocities = all_values[n_coord..].to_vec();
554 }
555
556 Ok(rst)
557 }
558}
559
560fn write_f64_block<W: Write>(
563 writer: &mut BufWriter<W>,
564 values: &[f64],
565 per_line: usize,
566) -> Result<(), AmberIoError> {
567 for (i, v) in values.iter().enumerate() {
568 write!(writer, "{:12.7}", v).map_err(|e| AmberIoError::IoError(e.to_string()))?;
569 if (i + 1) % per_line == 0 {
570 writeln!(writer).map_err(|e| AmberIoError::IoError(e.to_string()))?;
571 }
572 }
573 if !values.is_empty() && !values.len().is_multiple_of(per_line) {
574 writeln!(writer).map_err(|e| AmberIoError::IoError(e.to_string()))?;
575 }
576 Ok(())
577}
578
579pub struct MdcrdReader<R: BufRead> {
592 reader: R,
593 pub natom: usize,
595 pub has_box: bool,
597 title_consumed: bool,
598}
599
600impl<R: BufRead> MdcrdReader<R> {
601 pub fn new(reader: R, natom: usize, has_box: bool) -> Self {
606 MdcrdReader {
607 reader,
608 natom,
609 has_box,
610 title_consumed: false,
611 }
612 }
613
614 fn consume_title(&mut self) -> Result<String, AmberIoError> {
616 let mut title = String::new();
617 self.reader
618 .read_line(&mut title)
619 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
620 self.title_consumed = true;
621 Ok(title.trim_end().to_string())
622 }
623
624 pub fn read_frame(&mut self) -> Result<Option<MdcrdFrame>, AmberIoError> {
628 if !self.title_consumed {
629 self.consume_title()?;
630 }
631
632 let n_coords = self.natom * 3;
633 let mut values: Vec<f64> = Vec::with_capacity(n_coords);
634
635 loop {
636 if values.len() >= n_coords {
637 break;
638 }
639 let mut line = String::new();
640 let bytes = self
641 .reader
642 .read_line(&mut line)
643 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
644 if bytes == 0 {
645 if values.is_empty() {
646 return Ok(None);
647 }
648 break;
649 }
650 for tok in line.split_whitespace() {
651 values.push(
652 tok.parse::<f64>()
653 .map_err(|e| AmberIoError::ParseError(e.to_string()))?,
654 );
655 if values.len() >= n_coords {
656 break;
657 }
658 }
659 }
660
661 if values.is_empty() {
662 return Ok(None);
663 }
664
665 let box_dims = if self.has_box {
666 let mut line = String::new();
667 self.reader
668 .read_line(&mut line)
669 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
670 let parts: Vec<f64> = line
671 .split_whitespace()
672 .filter_map(|t| t.parse::<f64>().ok())
673 .collect();
674 if parts.len() >= 3 {
675 Some([parts[0], parts[1], parts[2]])
676 } else {
677 None
678 }
679 } else {
680 None
681 };
682
683 Ok(Some(MdcrdFrame {
684 coordinates: values,
685 box_dims,
686 }))
687 }
688}
689
690#[derive(Debug, Clone)]
692pub struct MdcrdFrame {
693 pub coordinates: Vec<f64>,
695 pub box_dims: Option<[f64; 3]>,
697}
698
699impl MdcrdFrame {
700 pub fn natom(&self) -> usize {
702 self.coordinates.len() / 3
703 }
704
705 pub fn atom_pos(&self, idx: usize) -> Option<(f64, f64, f64)> {
707 let base = idx * 3;
708 if base + 2 < self.coordinates.len() {
709 Some((
710 self.coordinates[base],
711 self.coordinates[base + 1],
712 self.coordinates[base + 2],
713 ))
714 } else {
715 None
716 }
717 }
718}
719
720#[derive(Debug, Clone, Default)]
729pub struct MdinFile {
730 pub title: String,
732 pub cntrl: HashMap<String, String>,
734 pub ewald: HashMap<String, String>,
736}
737
738impl MdinFile {
739 pub fn new() -> Self {
741 MdinFile::default()
742 }
743
744 pub fn cntrl_int(&self, key: &str, default: i64) -> i64 {
746 self.cntrl
747 .get(key)
748 .and_then(|v| v.trim().parse().ok())
749 .unwrap_or(default)
750 }
751
752 pub fn cntrl_float(&self, key: &str, default: f64) -> f64 {
754 self.cntrl
755 .get(key)
756 .and_then(|v| v.trim().parse().ok())
757 .unwrap_or(default)
758 }
759
760 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), AmberIoError> {
762 let mut bw = BufWriter::new(writer);
763 writeln!(bw, "{}", self.title).map_err(|e| AmberIoError::IoError(e.to_string()))?;
764 writeln!(bw, " &cntrl").map_err(|e| AmberIoError::IoError(e.to_string()))?;
765 let mut keys: Vec<&String> = self.cntrl.keys().collect();
766 keys.sort();
767 for k in &keys {
768 writeln!(bw, " {} = {},", k, self.cntrl[*k])
769 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
770 }
771 writeln!(bw, " /").map_err(|e| AmberIoError::IoError(e.to_string()))?;
772 Ok(())
773 }
774
775 pub fn read<R: Read>(reader: R) -> Result<Self, AmberIoError> {
777 let br = BufReader::new(reader);
778 let mut file = MdinFile::new();
779 let mut lines = br.lines().peekable();
780
781 file.title = lines
783 .next()
784 .ok_or(AmberIoError::UnexpectedEof)?
785 .map_err(|e| AmberIoError::IoError(e.to_string()))?;
786
787 let mut current_nl: Option<&str> = None;
788 for line_result in lines {
789 let line = line_result.map_err(|e| AmberIoError::IoError(e.to_string()))?;
790 let trimmed = line.trim();
791
792 if trimmed.starts_with('&') {
793 let nl_name = trimmed.trim_start_matches('&').to_lowercase();
794 current_nl = match nl_name.as_str() {
795 "cntrl" => Some("cntrl"),
796 "ewald" => Some("ewald"),
797 _ => None,
798 };
799 continue;
800 }
801
802 if trimmed == "/" || trimmed == "&end" || trimmed.to_lowercase() == "end" {
803 current_nl = None;
804 continue;
805 }
806
807 if let Some(nl) = current_nl {
808 for entry in trimmed.split(',') {
810 let entry = entry.trim();
811 if entry.is_empty() {
812 continue;
813 }
814 if let Some(pos) = entry.find('=') {
815 let key = entry[..pos].trim().to_lowercase();
816 let val = entry[pos + 1..].trim().to_string();
817 if !key.is_empty() {
818 match nl {
819 "cntrl" => {
820 file.cntrl.insert(key, val);
821 }
822 "ewald" => {
823 file.ewald.insert(key, val);
824 }
825 _ => {}
826 }
827 }
828 }
829 }
830 }
831 }
832
833 Ok(file)
834 }
835}
836
837#[derive(Debug, Clone, Default)]
843pub struct MdinfoSnapshot {
844 pub nstep: i64,
846 pub time: f64,
848 pub temp: f64,
850 pub press: f64,
852 pub etot: f64,
854 pub ektot: f64,
856 pub eptot: f64,
858}
859
860pub struct MdinfoParser;
862
863impl MdinfoParser {
864 pub fn parse<R: Read>(reader: R) -> Result<MdinfoSnapshot, AmberIoError> {
873 let br = BufReader::new(reader);
874 let mut snap = MdinfoSnapshot::default();
875
876 for line_result in br.lines() {
877 let line = line_result.map_err(|e| AmberIoError::IoError(e.to_string()))?;
878 let chars: Vec<char> = line.chars().collect();
882 let mut i = 0usize;
883 while i < chars.len() {
884 while i < chars.len() && chars[i].is_whitespace() {
886 i += 1;
887 }
888 let key_start = i;
890 while i < chars.len() && chars[i] != '=' {
891 i += 1;
892 }
893 if i >= chars.len() {
894 break;
895 }
896 let key: String = chars[key_start..i]
897 .iter()
898 .collect::<String>()
899 .trim()
900 .to_uppercase();
901 i += 1; while i < chars.len() && chars[i].is_whitespace() {
904 i += 1;
905 }
906 let val_start = i;
907 while i < chars.len() && !chars[i].is_whitespace() {
908 i += 1;
909 }
910 let val_str: String = chars[val_start..i].iter().collect();
911 let val: f64 = match val_str.parse() {
912 Ok(v) => v,
913 Err(_) => continue,
914 };
915 match key.as_str() {
916 "NSTEP" => snap.nstep = val as i64,
917 "TIME(PS)" => snap.time = val,
918 "TEMP(K)" => snap.temp = val,
919 "PRESS" => snap.press = val,
920 "ETOT" => snap.etot = val,
921 "EKTOT" => snap.ektot = val,
922 "EPTOT" => snap.eptot = val,
923 _ => {}
924 }
925 }
926 }
927
928 Ok(snap)
929 }
930}
931
932#[derive(Debug, Clone)]
938pub struct FfMass {
939 pub atom_type: String,
941 pub mass: f64,
943 pub polarizability: f64,
945}
946
947#[derive(Debug, Clone)]
949pub struct FfBond {
950 pub type_i: String,
952 pub type_j: String,
954 pub force_constant: f64,
956 pub eq_length: f64,
958}
959
960#[derive(Debug, Clone)]
962pub struct FfAngle {
963 pub type_i: String,
965 pub type_j: String,
967 pub type_k: String,
969 pub force_constant: f64,
971 pub eq_angle_deg: f64,
973}
974
975#[derive(Debug, Clone)]
977pub struct FfDihedral {
978 pub type_i: String,
980 pub type_j: String,
982 pub type_k: String,
984 pub type_l: String,
986 pub periodicity: i32,
988 pub barrier: f64,
990 pub phase_deg: f64,
992}
993
994#[derive(Debug, Clone, Default)]
996pub struct AmberForceFieldReader {
997 pub masses: Vec<FfMass>,
999 pub bonds: Vec<FfBond>,
1001 pub angles: Vec<FfAngle>,
1003 pub dihedrals: Vec<FfDihedral>,
1005}
1006
1007impl AmberForceFieldReader {
1008 pub fn new() -> Self {
1010 AmberForceFieldReader::default()
1011 }
1012
1013 pub fn read<R: Read>(&mut self, reader: R) -> Result<(), AmberIoError> {
1015 let br = BufReader::new(reader);
1016 let lines: Vec<String> = br
1017 .lines()
1018 .map(|l| l.map_err(|e| AmberIoError::IoError(e.to_string())))
1019 .collect::<Result<_, _>>()?;
1020
1021 #[derive(PartialEq)]
1022 enum Section {
1023 None,
1024 Mass,
1025 Bond,
1026 Angle,
1027 Dihe,
1028 }
1029 let mut section = Section::None;
1030
1031 for line in &lines {
1032 let trimmed = line.trim();
1033 if trimmed.is_empty() {
1034 section = Section::None;
1035 continue;
1036 }
1037
1038 let upper = trimmed.to_uppercase();
1039 if upper.starts_with("MASS") {
1040 section = Section::Mass;
1041 continue;
1042 } else if upper.starts_with("BOND") {
1043 section = Section::Bond;
1044 continue;
1045 } else if upper.starts_with("ANGLE") || upper.starts_with("ANGL") {
1046 section = Section::Angle;
1047 continue;
1048 } else if upper.starts_with("DIHE") {
1049 section = Section::Dihe;
1050 continue;
1051 } else if upper.starts_with("NONB") || upper.starts_with("IMPR") {
1052 section = Section::None;
1053 continue;
1054 }
1055
1056 match section {
1057 Section::Mass => {
1058 let clean = trimmed.split('!').next().unwrap_or("").trim();
1060 let parts: Vec<&str> = clean.split_whitespace().collect();
1061 if parts.len() >= 2 {
1062 let mass = parts[1].parse::<f64>().unwrap_or(0.0);
1063 let pol = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0.0);
1064 self.masses.push(FfMass {
1065 atom_type: parts[0].to_string(),
1066 mass,
1067 polarizability: pol,
1068 });
1069 }
1070 }
1071 Section::Bond => {
1072 let clean = trimmed.split('!').next().unwrap_or("").trim();
1074 let parts: Vec<&str> = clean.split_whitespace().collect();
1075 if parts.len() >= 3 {
1076 let types: Vec<&str> = parts[0].split('-').collect();
1077 if types.len() >= 2 {
1078 let kf = parts[1].parse::<f64>().unwrap_or(0.0);
1079 let r0 = parts[2].parse::<f64>().unwrap_or(0.0);
1080 self.bonds.push(FfBond {
1081 type_i: types[0].trim().to_string(),
1082 type_j: types[1].trim().to_string(),
1083 force_constant: kf,
1084 eq_length: r0,
1085 });
1086 }
1087 }
1088 }
1089 Section::Angle => {
1090 let clean = trimmed.split('!').next().unwrap_or("").trim();
1092 let parts: Vec<&str> = clean.split_whitespace().collect();
1093 if parts.len() >= 3 {
1094 let types: Vec<&str> = parts[0].split('-').collect();
1095 if types.len() >= 3 {
1096 let kf = parts[1].parse::<f64>().unwrap_or(0.0);
1097 let th0 = parts[2].parse::<f64>().unwrap_or(0.0);
1098 self.angles.push(FfAngle {
1099 type_i: types[0].trim().to_string(),
1100 type_j: types[1].trim().to_string(),
1101 type_k: types[2].trim().to_string(),
1102 force_constant: kf,
1103 eq_angle_deg: th0,
1104 });
1105 }
1106 }
1107 }
1108 Section::Dihe => {
1109 let clean = trimmed.split('!').next().unwrap_or("").trim();
1115 let mut split_at = clean.len();
1116 let cbytes = clean.as_bytes();
1117 let mut idx = 0usize;
1118 while idx < cbytes.len() {
1119 if cbytes[idx].is_ascii_whitespace() {
1120 let ws_start = idx;
1121 while idx < cbytes.len() && cbytes[idx].is_ascii_whitespace() {
1122 idx += 1;
1123 }
1124 if idx < cbytes.len() {
1125 let rest = &clean[idx..];
1126 if rest
1127 .split_whitespace()
1128 .next()
1129 .map(|t| t.parse::<f64>().is_ok())
1130 .unwrap_or(false)
1131 {
1132 split_at = ws_start;
1133 break;
1134 }
1135 }
1136 } else {
1137 idx += 1;
1138 }
1139 }
1140 let type_field = clean[..split_at].trim();
1141 let remainder = clean[split_at..].trim();
1142 let normalised = type_field.replace(' ', "-");
1144 let types: Vec<&str> =
1145 normalised.split('-').filter(|s| !s.is_empty()).collect();
1146 let nums: Vec<&str> = remainder.split_whitespace().collect();
1147 if types.len() >= 4 && nums.len() >= 4 {
1148 let barrier = nums[1].parse::<f64>().unwrap_or(0.0);
1149 let phase = nums[2].parse::<f64>().unwrap_or(0.0);
1150 let pn = nums[3].parse::<f64>().unwrap_or(1.0).abs() as i32;
1151 self.dihedrals.push(FfDihedral {
1152 type_i: types[0].trim().to_string(),
1153 type_j: types[1].trim().to_string(),
1154 type_k: types[2].trim().to_string(),
1155 type_l: types[3].trim().to_string(),
1156 periodicity: pn,
1157 barrier,
1158 phase_deg: phase,
1159 });
1160 }
1161 }
1162 Section::None => {}
1163 }
1164 }
1165
1166 Ok(())
1167 }
1168}
1169
1170pub const AMBER_CHARGE_UNIT: f64 = 18.2223;
1180
1181pub fn amber_charge_to_e(amber_q: f64) -> f64 {
1183 amber_q / AMBER_CHARGE_UNIT
1184}
1185
1186pub fn e_to_amber_charge(q_e: f64) -> f64 {
1188 q_e * AMBER_CHARGE_UNIT
1189}
1190
1191#[cfg(test)]
1196mod tests {
1197 use super::*;
1198 use std::io::Cursor;
1199
1200 #[test]
1203 fn test_prmtop_atom_new() {
1204 let a = PrmtopAtom::new("CA", 0.0, 12.011, "CT");
1205 assert_eq!(a.name, "CA");
1206 assert_eq!(a.mass, 12.011);
1207 }
1208
1209 #[test]
1210 fn test_prmtop_atom_charge_si() {
1211 let a = PrmtopAtom::new("N", 18.2223, 14.007, "N");
1212 let q = a.charge_si();
1213 assert!((q - 1.0).abs() < 1e-6, "expected ~1.0, got {q}");
1214 }
1215
1216 #[test]
1217 fn test_prmtop_atom_charge_negative() {
1218 let a = PrmtopAtom::new("O", -0.8 * 18.2223, 15.999, "OS");
1219 assert!(a.charge_si() < 0.0);
1220 }
1221
1222 #[test]
1225 fn test_prmtop_bond_new() {
1226 let b = PrmtopBond::new(0, 1, 340.0, 1.526);
1227 assert!(b.force_constant > 0.0);
1228 assert!(b.eq_length > 0.0);
1229 }
1230
1231 #[test]
1232 fn test_prmtop_bond_energy_at_eq() {
1233 let b = PrmtopBond::new(0, 1, 340.0, 1.526);
1234 let e = b.energy(b.eq_length);
1235 assert!(e.abs() < 1e-12, "energy at eq length should be ~0, got {e}");
1236 }
1237
1238 #[test]
1239 fn test_prmtop_bond_energy_positive() {
1240 let b = PrmtopBond::new(0, 1, 340.0, 1.526);
1241 let e = b.energy(1.7);
1242 assert!(e > 0.0);
1243 }
1244
1245 #[test]
1246 fn test_prmtop_bond_energy_symmetric() {
1247 let b = PrmtopBond::new(0, 1, 340.0, 1.526);
1248 let e1 = b.energy(1.526 + 0.1);
1249 let e2 = b.energy(1.526 - 0.1);
1250 assert!((e1 - e2).abs() < 1e-10);
1251 }
1252
1253 #[test]
1256 fn test_prmtop_angle_new() {
1257 let a = PrmtopAngle::new(0, 1, 2, 50.0, std::f64::consts::PI * 109.5 / 180.0);
1258 assert_eq!(a.atom_j, 1);
1259 }
1260
1261 #[test]
1262 fn test_prmtop_angle_energy_at_eq() {
1263 let theta0 = 109.5_f64.to_radians();
1264 let a = PrmtopAngle::new(0, 1, 2, 50.0, theta0);
1265 let e = a.energy(theta0);
1266 assert!(e.abs() < 1e-12);
1267 }
1268
1269 #[test]
1270 fn test_prmtop_angle_energy_positive_deviation() {
1271 let theta0 = 109.5_f64.to_radians();
1272 let a = PrmtopAngle::new(0, 1, 2, 50.0, theta0);
1273 let e = a.energy(theta0 + 0.2);
1274 assert!(e > 0.0);
1275 }
1276
1277 fn make_prmtop() -> PrmtopFile {
1280 let mut f = PrmtopFile::new();
1281 f.version = "V0001.000".to_string();
1282 f.atoms.push(PrmtopAtom::new(
1283 "N",
1284 -0.4157 * AMBER_CHARGE_UNIT,
1285 14.007,
1286 "N3",
1287 ));
1288 f.atoms.push(PrmtopAtom::new(
1289 "H1",
1290 0.2719 * AMBER_CHARGE_UNIT,
1291 1.008,
1292 "H",
1293 ));
1294 f.atoms.push(PrmtopAtom::new(
1295 "CA",
1296 -0.0249 * AMBER_CHARGE_UNIT,
1297 12.011,
1298 "CT",
1299 ));
1300 f.bonds.push(PrmtopBond::new(0, 1, 434.0, 1.01));
1301 f.bonds.push(PrmtopBond::new(1, 2, 317.0, 1.522));
1302 f
1303 }
1304
1305 #[test]
1306 fn test_prmtop_roundtrip_natom() {
1307 let original = make_prmtop();
1308 let mut buf = Vec::new();
1309 original.write(&mut buf).unwrap();
1310 let restored = PrmtopFile::read(Cursor::new(&buf)).unwrap();
1311 assert_eq!(original.natom(), restored.natom());
1312 }
1313
1314 #[test]
1315 fn test_prmtop_roundtrip_atom_names() {
1316 let original = make_prmtop();
1317 let mut buf = Vec::new();
1318 original.write(&mut buf).unwrap();
1319 let restored = PrmtopFile::read(Cursor::new(&buf)).unwrap();
1320 for (a, b) in original.atoms.iter().zip(restored.atoms.iter()) {
1321 assert_eq!(a.name, b.name);
1322 }
1323 }
1324
1325 #[test]
1326 fn test_prmtop_roundtrip_charges() {
1327 let original = make_prmtop();
1328 let mut buf = Vec::new();
1329 original.write(&mut buf).unwrap();
1330 let restored = PrmtopFile::read(Cursor::new(&buf)).unwrap();
1331 for (a, b) in original.atoms.iter().zip(restored.atoms.iter()) {
1332 assert!(
1333 (a.charge - b.charge).abs() < 1e-4,
1334 "charge mismatch: {} vs {}",
1335 a.charge,
1336 b.charge
1337 );
1338 }
1339 }
1340
1341 #[test]
1342 fn test_prmtop_roundtrip_masses() {
1343 let original = make_prmtop();
1344 let mut buf = Vec::new();
1345 original.write(&mut buf).unwrap();
1346 let restored = PrmtopFile::read(Cursor::new(&buf)).unwrap();
1347 for (a, b) in original.atoms.iter().zip(restored.atoms.iter()) {
1348 assert!((a.mass - b.mass).abs() < 1e-4);
1349 }
1350 }
1351
1352 #[test]
1353 fn test_prmtop_roundtrip_bond_force_constants() {
1354 let original = make_prmtop();
1355 let mut buf = Vec::new();
1356 original.write(&mut buf).unwrap();
1357 let restored = PrmtopFile::read(Cursor::new(&buf)).unwrap();
1358 assert_eq!(original.bonds.len(), restored.bonds.len());
1359 for (a, b) in original.bonds.iter().zip(restored.bonds.iter()) {
1360 assert!(
1361 (a.force_constant - b.force_constant).abs() < 1e-3,
1362 "force_constant mismatch: {} vs {}",
1363 a.force_constant,
1364 b.force_constant
1365 );
1366 }
1367 }
1368
1369 #[test]
1370 fn test_prmtop_bond_parameters_positive() {
1371 let f = make_prmtop();
1372 for bond in &f.bonds {
1373 assert!(bond.force_constant > 0.0, "force constant must be positive");
1374 assert!(bond.eq_length > 0.0, "eq length must be positive");
1375 }
1376 }
1377
1378 #[test]
1379 fn test_prmtop_empty_roundtrip() {
1380 let original = PrmtopFile::new();
1381 let mut buf = Vec::new();
1382 original.write(&mut buf).unwrap();
1383 let restored = PrmtopFile::read(Cursor::new(&buf)).unwrap();
1384 assert_eq!(restored.natom(), 0);
1385 }
1386
1387 fn make_rst7(natom: usize) -> Rst7File {
1390 let mut r = Rst7File::new();
1391 r.title = "Test restart file".to_string();
1392 r.time = Some(0.5);
1393 r.coordinates = (0..natom * 3).map(|i| i as f64 * 0.1).collect();
1394 r.velocities = vec![0.01; natom * 3];
1395 r
1396 }
1397
1398 #[test]
1399 fn test_rst7_natom() {
1400 let r = make_rst7(20);
1401 assert_eq!(r.natom(), 20);
1402 }
1403
1404 #[test]
1405 fn test_rst7_coordinate_count_matches_natom() {
1406 let r = make_rst7(7);
1407 assert_eq!(r.coordinates.len(), r.natom() * 3);
1408 }
1409
1410 #[test]
1411 fn test_rst7_write_read_roundtrip_natom() {
1412 let original = make_rst7(10);
1413 let mut buf = Vec::new();
1414 original.write(&mut buf).unwrap();
1415 let restored = Rst7File::read(Cursor::new(&buf)).unwrap();
1416 assert_eq!(original.natom(), restored.natom());
1417 }
1418
1419 #[test]
1420 fn test_rst7_write_read_roundtrip_coordinates() {
1421 let original = make_rst7(5);
1422 let mut buf = Vec::new();
1423 original.write(&mut buf).unwrap();
1424 let restored = Rst7File::read(Cursor::new(&buf)).unwrap();
1425 for (a, b) in original.coordinates.iter().zip(restored.coordinates.iter()) {
1426 assert!((a - b).abs() < 1e-5, "coord mismatch: {a} vs {b}");
1427 }
1428 }
1429
1430 #[test]
1431 fn test_rst7_coordinate_format_12_7() {
1432 let r = make_rst7(1);
1434 let mut buf = Vec::new();
1435 r.write(&mut buf).unwrap();
1436 let text = String::from_utf8(buf).unwrap();
1437 let data_lines: Vec<&str> = text.lines().skip(2).collect();
1440 let first_data_line = data_lines[0];
1441 assert!(first_data_line.len() <= 72 + 1); }
1444
1445 #[test]
1446 fn test_rst7_time_preserved() {
1447 let original = make_rst7(3);
1448 let mut buf = Vec::new();
1449 original.write(&mut buf).unwrap();
1450 let restored = Rst7File::read(Cursor::new(&buf)).unwrap();
1451 assert!(restored.time.is_some());
1452 assert!((restored.time.unwrap() - 0.5).abs() < 1e-3);
1453 }
1454
1455 #[test]
1456 fn test_rst7_no_time() {
1457 let mut r = make_rst7(2);
1458 r.time = None;
1459 let mut buf = Vec::new();
1460 r.write(&mut buf).unwrap();
1461 let restored = Rst7File::read(Cursor::new(&buf)).unwrap();
1462 assert_eq!(restored.natom(), 2);
1463 }
1464
1465 fn make_mdcrd_text(natom: usize, nframes: usize) -> String {
1468 let mut s = String::from("TITLE : mdcrd test\n");
1469 let n_coord = natom * 3;
1470 for frame in 0..nframes {
1471 let vals: Vec<f64> = (0..n_coord)
1472 .map(|i| (frame * 100 + i) as f64 * 0.1)
1473 .collect();
1474 let mut count = 0usize;
1475 for v in &vals {
1476 s.push_str(&format!("{:8.3}", v));
1477 count += 1;
1478 if count.is_multiple_of(10) {
1479 s.push('\n');
1480 }
1481 }
1482 if !n_coord.is_multiple_of(10) {
1483 s.push('\n');
1484 }
1485 }
1486 s
1487 }
1488
1489 #[test]
1490 fn test_mdcrd_read_frame_count() {
1491 let natom = 5;
1492 let nframes = 4;
1493 let text = make_mdcrd_text(natom, nframes);
1494 let mut reader = MdcrdReader::new(BufReader::new(Cursor::new(text)), natom, false);
1495 let mut count = 0usize;
1496 while let Some(_frame) = reader.read_frame().unwrap() {
1497 count += 1;
1498 }
1499 assert_eq!(count, nframes);
1500 }
1501
1502 #[test]
1503 fn test_mdcrd_frame_natom() {
1504 let natom = 6;
1505 let text = make_mdcrd_text(natom, 1);
1506 let mut reader = MdcrdReader::new(BufReader::new(Cursor::new(text)), natom, false);
1507 let frame = reader.read_frame().unwrap().unwrap();
1508 assert_eq!(frame.natom(), natom);
1509 }
1510
1511 #[test]
1512 fn test_mdcrd_frame_coord_len() {
1513 let natom = 3;
1514 let text = make_mdcrd_text(natom, 1);
1515 let mut reader = MdcrdReader::new(BufReader::new(Cursor::new(text)), natom, false);
1516 let frame = reader.read_frame().unwrap().unwrap();
1517 assert_eq!(frame.coordinates.len(), natom * 3);
1518 }
1519
1520 #[test]
1521 fn test_mdcrd_atom_pos_valid() {
1522 let natom = 4;
1523 let text = make_mdcrd_text(natom, 1);
1524 let mut reader = MdcrdReader::new(BufReader::new(Cursor::new(text)), natom, false);
1525 let frame = reader.read_frame().unwrap().unwrap();
1526 assert!(frame.atom_pos(0).is_some());
1527 assert!(frame.atom_pos(natom - 1).is_some());
1528 }
1529
1530 #[test]
1531 fn test_mdcrd_atom_pos_out_of_bounds() {
1532 let natom = 2;
1533 let text = make_mdcrd_text(natom, 1);
1534 let mut reader = MdcrdReader::new(BufReader::new(Cursor::new(text)), natom, false);
1535 let frame = reader.read_frame().unwrap().unwrap();
1536 assert!(frame.atom_pos(100).is_none());
1538 }
1539
1540 #[test]
1541 fn test_mdcrd_eof_returns_none() {
1542 let text = "TITLE\n";
1543 let mut reader = MdcrdReader::new(BufReader::new(Cursor::new(text)), 3, false);
1544 let result = reader.read_frame().unwrap();
1545 assert!(result.is_none());
1546 }
1547
1548 fn make_mdin_text() -> &'static str {
1551 "MD simulation test\n \
1552 &cntrl\n \
1553 nstlim = 1000,\n \
1554 dt = 0.002,\n \
1555 temp0 = 300.0,\n \
1556 ntpr = 100,\n \
1557 ntwx = 100,\n \
1558 ntb = 1,\n \
1559 /\n"
1560 }
1561
1562 #[test]
1563 fn test_mdin_parse_nstlim() {
1564 let f = MdinFile::read(Cursor::new(make_mdin_text())).unwrap();
1565 assert_eq!(f.cntrl_int("nstlim", 0), 1000);
1566 }
1567
1568 #[test]
1569 fn test_mdin_parse_dt() {
1570 let f = MdinFile::read(Cursor::new(make_mdin_text())).unwrap();
1571 let dt = f.cntrl_float("dt", 0.0);
1572 assert!((dt - 0.002).abs() < 1e-6);
1573 }
1574
1575 #[test]
1576 fn test_mdin_parse_temp0() {
1577 let f = MdinFile::read(Cursor::new(make_mdin_text())).unwrap();
1578 let temp = f.cntrl_float("temp0", 0.0);
1579 assert!((temp - 300.0).abs() < 1e-6);
1580 }
1581
1582 #[test]
1583 fn test_mdin_parse_ntpr() {
1584 let f = MdinFile::read(Cursor::new(make_mdin_text())).unwrap();
1585 assert_eq!(f.cntrl_int("ntpr", 0), 100);
1586 }
1587
1588 #[test]
1589 fn test_mdin_default_for_missing_key() {
1590 let f = MdinFile::read(Cursor::new(make_mdin_text())).unwrap();
1591 assert_eq!(f.cntrl_int("nonexistent", 42), 42);
1592 }
1593
1594 #[test]
1595 fn test_mdin_write_read_roundtrip() {
1596 let original = MdinFile::read(Cursor::new(make_mdin_text())).unwrap();
1597 let mut buf = Vec::new();
1598 original.write(&mut buf).unwrap();
1599 let restored = MdinFile::read(Cursor::new(&buf)).unwrap();
1600 assert_eq!(
1601 original.cntrl_int("nstlim", 0),
1602 restored.cntrl_int("nstlim", 0)
1603 );
1604 }
1605
1606 fn make_mdinfo_text() -> &'static str {
1609 " NSTEP = 500 TIME(PS) = 1.000\n \
1610 TEMP(K) = 298.15 PRESS = 0.00\n \
1611 ETOT = -5000.0 EKTOT = 1000.0 EPTOT = -6000.0\n"
1612 }
1613
1614 #[test]
1615 fn test_mdinfo_parse_nstep() {
1616 let s = MdinfoParser::parse(Cursor::new(make_mdinfo_text())).unwrap();
1617 assert_eq!(s.nstep, 500);
1618 }
1619
1620 #[test]
1621 fn test_mdinfo_parse_time() {
1622 let s = MdinfoParser::parse(Cursor::new(make_mdinfo_text())).unwrap();
1623 assert!((s.time - 1.0).abs() < 0.01);
1624 }
1625
1626 #[test]
1627 fn test_mdinfo_parse_temp() {
1628 let s = MdinfoParser::parse(Cursor::new(make_mdinfo_text())).unwrap();
1629 assert!((s.temp - 298.15).abs() < 0.1);
1630 }
1631
1632 #[test]
1633 fn test_mdinfo_parse_etot() {
1634 let s = MdinfoParser::parse(Cursor::new(make_mdinfo_text())).unwrap();
1635 assert!((s.etot - (-5000.0)).abs() < 1.0);
1636 }
1637
1638 #[test]
1639 fn test_mdinfo_energy_conservation() {
1640 let s = MdinfoParser::parse(Cursor::new(make_mdinfo_text())).unwrap();
1641 let sum = s.ektot + s.eptot;
1643 assert!((s.etot - sum).abs() < 1.0);
1644 }
1645
1646 fn make_ff_text() -> &'static str {
1649 "AMBER force field parameters\n\
1650 MASS\n\
1651 c3 12.010 !sp3 carbon\n\
1652 hc 1.008 !H on aliphatic C\n\
1653 n3 14.010 !nitrogen sp3\n\
1654 \n\
1655 BOND\n\
1656 c3-hc 340.0 1.09 !C-H aliphatic\n\
1657 c3-n3 337.0 1.47 !C-N single\n\
1658 \n\
1659 ANGLE\n\
1660 hc-c3-hc 35.0 109.50 !tetrahedral H-C-H\n\
1661 hc-c3-n3 50.0 109.50 !H-C-N\n\
1662 \n\
1663 DIHE\n\
1664 X -c3-n3-X 1 1.40 180.0 2.0 !sp3 C-N dihedral\n\
1665 \n"
1666 }
1667
1668 #[test]
1669 fn test_ff_mass_count() {
1670 let mut r = AmberForceFieldReader::new();
1671 r.read(Cursor::new(make_ff_text())).unwrap();
1672 assert_eq!(r.masses.len(), 3);
1673 }
1674
1675 #[test]
1676 fn test_ff_bond_count() {
1677 let mut r = AmberForceFieldReader::new();
1678 r.read(Cursor::new(make_ff_text())).unwrap();
1679 assert_eq!(r.bonds.len(), 2);
1680 }
1681
1682 #[test]
1683 fn test_ff_angle_count() {
1684 let mut r = AmberForceFieldReader::new();
1685 r.read(Cursor::new(make_ff_text())).unwrap();
1686 assert_eq!(r.angles.len(), 2);
1687 }
1688
1689 #[test]
1690 fn test_ff_dihedral_count() {
1691 let mut r = AmberForceFieldReader::new();
1692 r.read(Cursor::new(make_ff_text())).unwrap();
1693 assert_eq!(r.dihedrals.len(), 1);
1694 }
1695
1696 #[test]
1697 fn test_ff_bond_force_constants_positive() {
1698 let mut r = AmberForceFieldReader::new();
1699 r.read(Cursor::new(make_ff_text())).unwrap();
1700 for b in &r.bonds {
1701 assert!(
1702 b.force_constant > 0.0,
1703 "bond force constant must be positive, got {}",
1704 b.force_constant
1705 );
1706 }
1707 }
1708
1709 #[test]
1710 fn test_ff_bond_eq_lengths_positive() {
1711 let mut r = AmberForceFieldReader::new();
1712 r.read(Cursor::new(make_ff_text())).unwrap();
1713 for b in &r.bonds {
1714 assert!(
1715 b.eq_length > 0.0,
1716 "bond eq length must be positive, got {}",
1717 b.eq_length
1718 );
1719 }
1720 }
1721
1722 #[test]
1723 fn test_ff_angle_force_constants_positive() {
1724 let mut r = AmberForceFieldReader::new();
1725 r.read(Cursor::new(make_ff_text())).unwrap();
1726 for a in &r.angles {
1727 assert!(a.force_constant > 0.0);
1728 }
1729 }
1730
1731 #[test]
1732 fn test_ff_mass_values() {
1733 let mut r = AmberForceFieldReader::new();
1734 r.read(Cursor::new(make_ff_text())).unwrap();
1735 let c3 = r.masses.iter().find(|m| m.atom_type == "c3").unwrap();
1736 assert!((c3.mass - 12.01).abs() < 0.1);
1737 }
1738
1739 #[test]
1742 fn test_amber_charge_to_e_unit() {
1743 let q = amber_charge_to_e(AMBER_CHARGE_UNIT);
1744 assert!((q - 1.0).abs() < 1e-10);
1745 }
1746
1747 #[test]
1748 fn test_e_to_amber_charge_unit() {
1749 let q = e_to_amber_charge(1.0);
1750 assert!((q - AMBER_CHARGE_UNIT).abs() < 1e-10);
1751 }
1752
1753 #[test]
1754 fn test_amber_charge_roundtrip() {
1755 let original = 0.6;
1756 let roundtripped = amber_charge_to_e(e_to_amber_charge(original));
1757 assert!((original - roundtripped).abs() < 1e-12);
1758 }
1759
1760 #[test]
1761 fn test_amber_charge_factor_value() {
1762 assert!((AMBER_CHARGE_UNIT - 18.2223).abs() < 1e-4);
1764 }
1765
1766 #[test]
1767 fn test_amber_charge_negative() {
1768 let q_si = -0.5;
1769 let q_amber = e_to_amber_charge(q_si);
1770 assert!(q_amber < 0.0);
1771 assert!((amber_charge_to_e(q_amber) - q_si).abs() < 1e-12);
1772 }
1773}