1#![allow(
6 clippy::if_same_then_else,
7 clippy::manual_strip,
8 clippy::should_implement_trait
9)]
10#[allow(unused_imports)]
11use super::functions::*;
12
13#[allow(dead_code)]
15pub struct AmberBond {
16 pub i: usize,
18 pub j: usize,
20 pub k: f64,
22 pub r0: f64,
24}
25#[allow(dead_code)]
33#[derive(Debug, Clone, PartialEq)]
34pub struct AmberMask {
35 pub residues: Vec<usize>,
37 pub atom_names: Vec<String>,
39}
40#[allow(dead_code)]
41impl AmberMask {
42 pub fn parse(mask: &str) -> Result<Self, String> {
54 let mask = mask.trim();
55 if mask.is_empty() {
56 return Ok(AmberMask {
57 residues: Vec::new(),
58 atom_names: Vec::new(),
59 });
60 }
61 let (res_part, atom_part) = if let Some(at_pos) = mask.find('@') {
62 (&mask[..at_pos], Some(&mask[at_pos + 1..]))
63 } else {
64 (mask, None)
65 };
66 let residues = if res_part.starts_with(':') {
67 parse_residue_selection(&res_part[1..])?
68 } else if res_part.is_empty() {
69 Vec::new()
70 } else {
71 return Err(format!(
72 "Invalid mask: expected ':' before residue selection, got '{}'",
73 res_part
74 ));
75 };
76 let atom_names = if let Some(ap) = atom_part {
77 ap.split(',')
78 .map(|s| s.trim().to_string())
79 .filter(|s| !s.is_empty())
80 .collect()
81 } else {
82 Vec::new()
83 };
84 Ok(AmberMask {
85 residues,
86 atom_names,
87 })
88 }
89 pub fn matches_residue(&self, res_num: usize) -> bool {
91 if self.residues.is_empty() {
92 true
93 } else {
94 self.residues.contains(&res_num)
95 }
96 }
97 pub fn matches_atom(&self, atom_name: &str) -> bool {
99 if self.atom_names.is_empty() {
100 true
101 } else {
102 self.atom_names.iter().any(|n| n == atom_name)
103 }
104 }
105 pub fn matches(&self, res_num: usize, atom_name: &str) -> bool {
107 self.matches_residue(res_num) && self.matches_atom(atom_name)
108 }
109}
110pub struct AmberTopology {
112 pub title: String,
114 pub atoms: Vec<AmberAtom>,
116 pub bonds: Vec<AmberBond>,
118 pub angles: Vec<AmberAngle>,
120 pub n_atoms: usize,
122 pub n_bonds: usize,
124}
125impl AmberTopology {
126 pub fn from_prmtop_str(s: &str) -> Result<Self, String> {
131 let title = {
132 let raw = parse_flag_section(s, "TITLE")
133 .or_else(|| parse_flag_section(s, "title"))
134 .unwrap_or_default();
135 raw.trim().to_string()
136 };
137 let pointers = parse_flag_section(s, "POINTERS")
138 .map(|raw| parse_fortran_ints(&raw))
139 .unwrap_or_default();
140 let n_atoms = pointers.first().copied().unwrap_or(0) as usize;
141 let n_bonds = pointers.get(2).copied().unwrap_or(0) as usize;
142 let n_angles = pointers.get(4).copied().unwrap_or(0) as usize;
143 let atom_names: Vec<String> = parse_flag_section(s, "ATOM_NAME")
144 .map(|raw| parse_fortran_strings(&raw))
145 .unwrap_or_default();
146 const AMBER_CHARGE_SCALE: f64 = 18.2223;
147 let raw_charges: Vec<f64> = parse_flag_section(s, "CHARGE")
148 .map(|raw| parse_fortran_reals(&raw))
149 .unwrap_or_default();
150 let charges: Vec<f64> = raw_charges
151 .iter()
152 .map(|&q| q / AMBER_CHARGE_SCALE)
153 .collect();
154 let masses: Vec<f64> = parse_flag_section(s, "MASS")
155 .map(|raw| parse_fortran_reals(&raw))
156 .unwrap_or_default();
157 let atom_types: Vec<String> = parse_flag_section(s, "AMBER_ATOM_TYPE")
158 .map(|raw| parse_fortran_strings(&raw))
159 .unwrap_or_default();
160 let residue_labels: Vec<String> = parse_flag_section(s, "RESIDUE_LABEL")
161 .map(|raw| parse_fortran_strings(&raw))
162 .unwrap_or_default();
163 let residue_ptrs: Vec<i64> = parse_flag_section(s, "RESIDUE_POINTER")
164 .map(|raw| parse_fortran_ints(&raw))
165 .unwrap_or_default();
166 let atom_residue: Vec<String> = (0..n_atoms)
167 .map(|atom_idx| {
168 let res_idx = residue_ptrs
169 .iter()
170 .rposition(|&p| (p as usize) <= atom_idx + 1)
171 .unwrap_or(0);
172 residue_labels
173 .get(res_idx)
174 .cloned()
175 .unwrap_or_else(|| "UNK".to_string())
176 })
177 .collect();
178 let atoms: Vec<AmberAtom> = (0..n_atoms)
179 .map(|i| AmberAtom {
180 name: atom_names.get(i).cloned().unwrap_or_default(),
181 residue_name: atom_residue.get(i).cloned().unwrap_or_default(),
182 charge: charges.get(i).copied().unwrap_or(0.0),
183 mass: masses.get(i).copied().unwrap_or(0.0),
184 atom_type: atom_types.get(i).cloned().unwrap_or_default(),
185 })
186 .collect();
187 let bond_ints_h: Vec<i64> = parse_flag_section(s, "BONDS_INC_HYDROGEN")
188 .map(|raw| parse_fortran_ints(&raw))
189 .unwrap_or_default();
190 let bond_ints_no_h: Vec<i64> = parse_flag_section(s, "BONDS_WITHOUT_HYDROGEN")
191 .map(|raw| parse_fortran_ints(&raw))
192 .unwrap_or_default();
193 let bond_force_k: Vec<f64> = parse_flag_section(s, "BOND_FORCE_CONSTANT")
194 .map(|raw| parse_fortran_reals(&raw))
195 .unwrap_or_default();
196 let bond_equil_r: Vec<f64> = parse_flag_section(s, "BOND_EQUIL_VALUE")
197 .map(|raw| parse_fortran_reals(&raw))
198 .unwrap_or_default();
199 let mut bonds: Vec<AmberBond> = build_bonds(&bond_ints_h, &bond_force_k, &bond_equil_r);
200 bonds.extend(build_bonds(&bond_ints_no_h, &bond_force_k, &bond_equil_r));
201 let angle_ints_h: Vec<i64> = parse_flag_section(s, "ANGLES_INC_HYDROGEN")
202 .map(|raw| parse_fortran_ints(&raw))
203 .unwrap_or_default();
204 let angle_ints_no_h: Vec<i64> = parse_flag_section(s, "ANGLES_WITHOUT_HYDROGEN")
205 .map(|raw| parse_fortran_ints(&raw))
206 .unwrap_or_default();
207 let angle_force_k: Vec<f64> = parse_flag_section(s, "ANGLE_FORCE_CONSTANT")
208 .map(|raw| parse_fortran_reals(&raw))
209 .unwrap_or_default();
210 let angle_equil_t: Vec<f64> = parse_flag_section(s, "ANGLE_EQUIL_VALUE")
211 .map(|raw| parse_fortran_reals(&raw))
212 .unwrap_or_default();
213 let mut angles: Vec<AmberAngle> =
214 build_angles(&angle_ints_h, &angle_force_k, &angle_equil_t);
215 angles.extend(build_angles(
216 &angle_ints_no_h,
217 &angle_force_k,
218 &angle_equil_t,
219 ));
220 let n_bonds_actual = n_bonds.max(bonds.len());
221 let _ = n_angles;
222 Ok(AmberTopology {
223 title,
224 atoms,
225 bonds,
226 angles,
227 n_atoms,
228 n_bonds: n_bonds_actual,
229 })
230 }
231 pub fn total_charge(&self) -> f64 {
233 self.atoms.iter().map(|a| a.charge).sum()
234 }
235 pub fn total_mass(&self) -> f64 {
237 self.atoms.iter().map(|a| a.mass).sum()
238 }
239 pub fn residue_names(&self) -> Vec<String> {
241 let mut names: Vec<String> = self.atoms.iter().map(|a| a.residue_name.clone()).collect();
242 names.sort();
243 names.dedup();
244 names
245 }
246 pub fn write_summary(&self) -> String {
248 let mut s = String::new();
249 s.push_str(&format!("Title : {}\n", self.title));
250 s.push_str(&format!("Atoms : {}\n", self.atoms.len()));
251 s.push_str(&format!("Bonds : {}\n", self.bonds.len()));
252 s.push_str(&format!("Angles : {}\n", self.angles.len()));
253 s.push_str(&format!("Total charge : {:.4} e\n", self.total_charge()));
254 s.push_str(&format!("Total mass : {:.4} amu\n", self.total_mass()));
255 let res = self.residue_names();
256 s.push_str(&format!("Residues : {}\n", res.join(", ")));
257 s
258 }
259}
260impl AmberTopology {
261 #[allow(dead_code)]
263 pub fn atom_type_names(&self) -> Vec<String> {
264 let mut names: Vec<String> = self.atoms.iter().map(|a| a.atom_type.clone()).collect();
265 names.sort();
266 names.dedup();
267 names
268 }
269 #[allow(dead_code)]
271 pub fn n_atom_types(&self) -> usize {
272 self.atom_type_names().len()
273 }
274 #[allow(dead_code)]
276 pub fn atoms_in_residue(&self, res_name: &str) -> Vec<usize> {
277 self.atoms
278 .iter()
279 .enumerate()
280 .filter(|(_, a)| a.residue_name == res_name)
281 .map(|(i, _)| i)
282 .collect()
283 }
284}
285#[allow(dead_code)]
293#[derive(Debug, Clone)]
294pub struct AmberEnergyFrame {
295 pub step: u64,
297 pub time_ps: f64,
299 pub temp_k: f64,
301 pub e_tot: f64,
303 pub e_kin: f64,
305 pub e_pot: f64,
307}
308#[allow(dead_code)]
310#[derive(Debug, Clone)]
311pub struct FrcmodDihedral {
312 pub atom_types: String,
314 pub n_fold: f64,
316 pub v_n: f64,
318 pub gamma_deg: f64,
320 pub periodicity: f64,
322}
323#[allow(dead_code)]
325#[derive(Debug, Clone)]
326pub struct FrcmodAngle {
327 pub atom_types: String,
329 pub k: f64,
331 pub theta0_deg: f64,
333}
334#[allow(dead_code)]
336#[derive(Debug, Clone)]
337pub struct FrcmodNonbonded {
338 pub atom_type: String,
340 pub r_star: f64,
342 pub epsilon: f64,
344}
345#[allow(dead_code)]
347#[derive(Debug, Clone)]
348pub struct MdcrdFrame {
349 pub coords: Vec<f64>,
351 pub box_dims: Option<[f64; 3]>,
353}
354#[allow(dead_code)]
355impl MdcrdFrame {
356 pub fn position(&self, i: usize) -> Option<[f64; 3]> {
358 let base = i * 3;
359 if base + 2 < self.coords.len() {
360 Some([
361 self.coords[base],
362 self.coords[base + 1],
363 self.coords[base + 2],
364 ])
365 } else {
366 None
367 }
368 }
369 pub fn n_atoms(&self) -> usize {
371 self.coords.len() / 3
372 }
373}
374#[allow(dead_code)]
376#[derive(Debug, Clone, Default)]
377pub struct FrcmodFile {
378 pub title: String,
380 pub bonds: Vec<FrcmodBond>,
382 pub angles: Vec<FrcmodAngle>,
384 pub dihedrals: Vec<FrcmodDihedral>,
386 pub nonbonded: Vec<FrcmodNonbonded>,
388}
389#[allow(dead_code)]
390impl FrcmodFile {
391 pub fn from_str(s: &str) -> Result<Self, String> {
401 let mut frc = FrcmodFile::default();
402 let mut section = FrcmodSection::None;
403 let mut first_line = true;
404 for line in s.lines() {
405 let trimmed = line.trim();
406 if first_line {
407 frc.title = trimmed.to_string();
408 first_line = false;
409 continue;
410 }
411 let upper = trimmed.to_uppercase();
412 if upper.starts_with("MASS") {
413 section = FrcmodSection::Mass;
414 continue;
415 } else if upper.starts_with("BOND") {
416 section = FrcmodSection::Bond;
417 continue;
418 } else if upper.starts_with("ANGL") {
419 section = FrcmodSection::Angle;
420 continue;
421 } else if upper.starts_with("DIHE") {
422 section = FrcmodSection::Dihe;
423 continue;
424 } else if upper.starts_with("IMPR") {
425 section = FrcmodSection::Impr;
426 continue;
427 } else if upper.starts_with("NONB") {
428 section = FrcmodSection::Nonb;
429 continue;
430 } else if upper.starts_with("END") {
431 section = FrcmodSection::None;
432 continue;
433 }
434 if trimmed.is_empty() {
435 continue;
436 }
437 match section {
438 FrcmodSection::Bond => {
439 let data = strip_amber_comment(trimmed);
440 let parts: Vec<&str> = data
441 .splitn(3, char::is_whitespace)
442 .filter(|s| !s.is_empty())
443 .collect();
444 let nums = collect_numbers(data);
445 if !parts.is_empty() && nums.len() >= 2 {
446 frc.bonds.push(FrcmodBond {
447 atom_types: parts[0].to_string(),
448 k: nums[0],
449 r0: nums[1],
450 });
451 }
452 }
453 FrcmodSection::Angle => {
454 let data = strip_amber_comment(trimmed);
455 let parts: Vec<&str> = data
456 .splitn(2, char::is_whitespace)
457 .filter(|s| !s.is_empty())
458 .collect();
459 let nums = collect_numbers(data);
460 if !parts.is_empty() && nums.len() >= 2 {
461 frc.angles.push(FrcmodAngle {
462 atom_types: parts[0].to_string(),
463 k: nums[0],
464 theta0_deg: nums[1],
465 });
466 }
467 }
468 FrcmodSection::Dihe | FrcmodSection::Impr => {
469 let data = strip_amber_comment(trimmed);
470 let parts: Vec<&str> = data
471 .splitn(2, char::is_whitespace)
472 .filter(|s| !s.is_empty())
473 .collect();
474 let nums = collect_numbers(data);
475 if !parts.is_empty() && nums.len() >= 4 {
476 frc.dihedrals.push(FrcmodDihedral {
477 atom_types: parts[0].to_string(),
478 n_fold: nums[0],
479 v_n: nums[1],
480 gamma_deg: nums[2],
481 periodicity: nums[3],
482 });
483 }
484 }
485 FrcmodSection::Nonb => {
486 let nums = collect_numbers(trimmed);
487 let parts: Vec<&str> = trimmed.split_whitespace().collect();
488 if !parts.is_empty() && nums.len() >= 2 {
489 frc.nonbonded.push(FrcmodNonbonded {
490 atom_type: parts[0].to_string(),
491 r_star: nums[0],
492 epsilon: nums[1],
493 });
494 }
495 }
496 _ => {}
497 }
498 }
499 Ok(frc)
500 }
501 pub fn n_bonds(&self) -> usize {
503 self.bonds.len()
504 }
505 pub fn n_angles(&self) -> usize {
507 self.angles.len()
508 }
509 pub fn n_dihedrals(&self) -> usize {
511 self.dihedrals.len()
512 }
513 pub fn n_nonbonded(&self) -> usize {
515 self.nonbonded.len()
516 }
517 pub fn get_bond(&self, types: &str) -> Option<&FrcmodBond> {
519 self.bonds.iter().find(|b| {
520 b.atom_types == types || {
521 let parts: Vec<&str> = types.split('-').collect();
522 if parts.len() == 2 {
523 let rev = format!("{}-{}", parts[1], parts[0]);
524 b.atom_types == rev
525 } else {
526 false
527 }
528 }
529 })
530 }
531 pub fn get_nonbonded(&self, atom_type: &str) -> Option<&FrcmodNonbonded> {
533 self.nonbonded.iter().find(|n| n.atom_type == atom_type)
534 }
535}
536#[derive(Debug, Clone, PartialEq)]
538pub(super) enum FrcmodSection {
539 None,
540 Mass,
541 Bond,
542 Angle,
543 Dihe,
544 Impr,
545 Nonb,
546}
547#[allow(dead_code)]
549#[derive(Debug, Clone, Default)]
550pub struct AmberRst7 {
551 pub title: String,
553 pub positions: Vec<[f64; 3]>,
555 pub velocities: Vec<[f64; 3]>,
557}
558#[allow(dead_code)]
559impl AmberRst7 {
560 pub fn new(title: &str, positions: Vec<[f64; 3]>) -> Self {
562 Self {
563 title: title.to_string(),
564 positions,
565 velocities: Vec::new(),
566 }
567 }
568 pub fn write_velocity(&mut self, velocities: Vec<[f64; 3]>) -> std::result::Result<(), String> {
573 if velocities.len() != self.positions.len() {
574 return Err(format!(
575 "velocity count {} != position count {}",
576 velocities.len(),
577 self.positions.len()
578 ));
579 }
580 self.velocities = velocities;
581 Ok(())
582 }
583 pub fn to_string_repr(&self) -> String {
585 write_inpcrd(
586 &self.title,
587 &self.positions,
588 if self.velocities.is_empty() {
589 None
590 } else {
591 Some(&self.velocities)
592 },
593 )
594 }
595 pub fn n_atoms(&self) -> usize {
597 self.positions.len()
598 }
599 pub fn has_velocities(&self) -> bool {
601 !self.velocities.is_empty()
602 }
603}
604#[allow(dead_code)]
606pub struct AmberAtom {
607 pub name: String,
609 pub residue_name: String,
611 pub charge: f64,
613 pub mass: f64,
615 pub atom_type: String,
617}
618#[allow(dead_code)]
620#[derive(Debug, Clone)]
621pub struct AmberDihedral {
622 pub i: usize,
624 pub j: usize,
626 pub k: usize,
628 pub l: usize,
630 pub v_n: f64,
632 pub gamma: f64,
634 pub n: i32,
636 pub is_improper: bool,
638}
639#[allow(dead_code)]
641#[derive(Debug, Clone)]
642pub struct FrcmodBond {
643 pub atom_types: String,
645 pub k: f64,
647 pub r0: f64,
649}
650#[allow(dead_code)]
652pub struct AmberCoordinates {
653 pub title: String,
655 pub n_atoms: usize,
657 pub coords: Vec<f64>,
659 pub velocities: Option<Vec<f64>>,
661 pub box_dimensions: Option<[f64; 6]>,
663}
664#[allow(dead_code)]
665impl AmberCoordinates {
666 pub fn from_str(s: &str) -> Result<Self, String> {
672 let lines: Vec<&str> = s.lines().collect();
673 if lines.is_empty() {
674 return Err("Empty coordinate file".to_string());
675 }
676 let title = lines[0].trim().to_string();
677 if lines.len() < 2 {
678 return Err("Missing atom count line".to_string());
679 }
680 let n_atoms: usize = lines[1]
681 .split_whitespace()
682 .next()
683 .ok_or("Missing atom count")?
684 .parse()
685 .map_err(|e: std::num::ParseIntError| e.to_string())?;
686 let mut all_vals: Vec<f64> = Vec::new();
687 for line in lines.iter().skip(2) {
688 for tok in line.split_whitespace() {
689 if let Ok(v) = tok.parse::<f64>() {
690 all_vals.push(v);
691 }
692 }
693 }
694 let n_coords = n_atoms * 3;
695 if all_vals.len() < n_coords {
696 return Err(format!(
697 "Expected {} coordinate values, got {}",
698 n_coords,
699 all_vals.len()
700 ));
701 }
702 let coords = all_vals[..n_coords].to_vec();
703 let velocities = if all_vals.len() >= n_coords * 2 {
704 Some(all_vals[n_coords..n_coords * 2].to_vec())
705 } else {
706 None
707 };
708 let remaining = if velocities.is_some() {
709 &all_vals[n_coords * 2..]
710 } else {
711 &all_vals[n_coords..]
712 };
713 let box_dimensions = if remaining.len() >= 6 {
714 Some([
715 remaining[0],
716 remaining[1],
717 remaining[2],
718 remaining[3],
719 remaining[4],
720 remaining[5],
721 ])
722 } else {
723 None
724 };
725 Ok(AmberCoordinates {
726 title,
727 n_atoms,
728 coords,
729 velocities,
730 box_dimensions,
731 })
732 }
733 pub fn position(&self, i: usize) -> [f64; 3] {
735 let base = i * 3;
736 [
737 self.coords[base],
738 self.coords[base + 1],
739 self.coords[base + 2],
740 ]
741 }
742 pub fn to_restart_string(&self) -> String {
744 let mut s = format!("{}\n", self.title);
745 s.push_str(&format!("{:5}\n", self.n_atoms));
746 for (i, &v) in self.coords.iter().enumerate() {
747 s.push_str(&format!("{:12.7}", v));
748 if (i + 1) % 6 == 0 {
749 s.push('\n');
750 }
751 }
752 if !self.coords.len().is_multiple_of(6) {
753 s.push('\n');
754 }
755 if let Some(ref vels) = self.velocities {
756 for (i, &v) in vels.iter().enumerate() {
757 s.push_str(&format!("{:12.7}", v));
758 if (i + 1) % 6 == 0 {
759 s.push('\n');
760 }
761 }
762 if vels.len() % 6 != 0 {
763 s.push('\n');
764 }
765 }
766 if let Some(ref bx) = self.box_dimensions {
767 for &v in bx {
768 s.push_str(&format!("{:12.7}", v));
769 }
770 s.push('\n');
771 }
772 s
773 }
774}
775#[allow(dead_code)]
777#[derive(Debug, Clone)]
778pub struct FfBondParam {
779 pub type_a: String,
781 pub type_b: String,
783 pub k: f64,
785 pub r0: f64,
787}
788#[allow(dead_code)]
790#[derive(Debug, Clone)]
791pub struct AmberRestraint {
792 pub iat1: usize,
794 pub iat2: usize,
796 pub r1: f64,
798 pub r2: f64,
800 pub r3: f64,
802 pub r4: f64,
804 pub rk2: f64,
806 pub rk3: f64,
808}
809impl AmberRestraint {
810 #[allow(dead_code)]
814 pub fn energy(&self, r: f64) -> f64 {
815 if r < self.r1 {
816 self.rk2 * (r - self.r2).powi(2)
817 } else if r < self.r2 {
818 self.rk2 * (r - self.r2).powi(2)
819 } else if r <= self.r3 {
820 0.0
821 } else if r <= self.r4 {
822 self.rk3 * (r - self.r3).powi(2)
823 } else {
824 self.rk3 * (r - self.r3).powi(2)
825 }
826 }
827}
828#[allow(dead_code)]
830#[derive(Debug, Clone)]
831pub struct LjParameter {
832 pub type_a: usize,
834 pub type_b: usize,
836 pub a_coeff: f64,
838 pub b_coeff: f64,
840}
841impl LjParameter {
842 pub fn r_min(&self) -> f64 {
846 if self.b_coeff.abs() < 1e-30 {
847 return 0.0;
848 }
849 (2.0 * self.a_coeff / self.b_coeff).powf(1.0 / 6.0)
850 }
851 pub fn epsilon(&self) -> f64 {
853 if self.a_coeff.abs() < 1e-30 {
854 return 0.0;
855 }
856 self.b_coeff * self.b_coeff / (4.0 * self.a_coeff)
857 }
858}
859#[allow(dead_code)]
861#[derive(Debug, Clone)]
862pub struct FfAngleParam {
863 pub type_i: String,
865 pub type_j: String,
867 pub type_k: String,
869 pub k: f64,
871 pub theta0_deg: f64,
873}
874#[allow(dead_code)]
876pub struct AmberAngle {
877 pub i: usize,
879 pub j: usize,
881 pub k_idx: usize,
883 pub k: f64,
885 pub theta0: f64,
887}
888#[allow(dead_code)]
890#[derive(Debug, Clone)]
891pub struct AmberBox {
892 pub a: f64,
894 pub b: f64,
896 pub c: f64,
898 pub alpha: f64,
900 pub beta: f64,
902 pub gamma: f64,
904}
905#[allow(dead_code)]
906impl AmberBox {
907 pub fn cubic(a: f64) -> Self {
909 AmberBox {
910 a,
911 b: a,
912 c: a,
913 alpha: 90.0,
914 beta: 90.0,
915 gamma: 90.0,
916 }
917 }
918 pub fn orthorhombic(a: f64, b: f64, c: f64) -> Self {
920 AmberBox {
921 a,
922 b,
923 c,
924 alpha: 90.0,
925 beta: 90.0,
926 gamma: 90.0,
927 }
928 }
929 pub fn is_cubic(&self) -> bool {
931 (self.a - self.b).abs() < 1e-8
932 && (self.b - self.c).abs() < 1e-8
933 && (self.alpha - 90.0).abs() < 1e-8
934 && (self.beta - 90.0).abs() < 1e-8
935 && (self.gamma - 90.0).abs() < 1e-8
936 }
937 pub fn is_orthorhombic(&self) -> bool {
939 (self.alpha - 90.0).abs() < 1e-8
940 && (self.beta - 90.0).abs() < 1e-8
941 && (self.gamma - 90.0).abs() < 1e-8
942 }
943 pub fn volume(&self) -> f64 {
945 use std::f64::consts::PI;
946 let to_rad = |deg: f64| deg * PI / 180.0;
947 let (ca, cb, cg) = (
948 to_rad(self.alpha).cos(),
949 to_rad(self.beta).cos(),
950 to_rad(self.gamma).cos(),
951 );
952 let v = (1.0 + 2.0 * ca * cb * cg - ca * ca - cb * cb - cg * cg).sqrt();
953 self.a * self.b * self.c * v
954 }
955 pub fn in_angstrom(&self) -> Self {
957 self.clone()
958 }
959 pub fn in_nm(&self) -> [f64; 6] {
961 [
962 self.a / 10.0,
963 self.b / 10.0,
964 self.c / 10.0,
965 self.alpha,
966 self.beta,
967 self.gamma,
968 ]
969 }
970}
971#[allow(dead_code)]
981pub struct AmberDcd {
982 pub frames: Vec<Vec<f32>>,
984 pub n_atoms: usize,
986}
987#[allow(dead_code)]
988impl AmberDcd {
989 pub fn new(n_atoms: usize) -> Self {
991 Self {
992 frames: Vec::new(),
993 n_atoms,
994 }
995 }
996 pub fn write_frame(&mut self, positions: &[[f64; 3]]) -> std::result::Result<(), String> {
1000 if positions.len() != self.n_atoms {
1001 return Err(format!(
1002 "expected {} atoms, got {}",
1003 self.n_atoms,
1004 positions.len()
1005 ));
1006 }
1007 let flat: Vec<f32> = positions
1008 .iter()
1009 .flat_map(|p| p.iter().map(|&v| v as f32))
1010 .collect();
1011 self.frames.push(flat);
1012 Ok(())
1013 }
1014 pub fn n_frames(&self) -> usize {
1016 self.frames.len()
1017 }
1018 pub fn get_position(&self, frame_idx: usize, atom_idx: usize) -> Option<[f32; 3]> {
1020 let frame = self.frames.get(frame_idx)?;
1021 let base = atom_idx * 3;
1022 if base + 2 >= frame.len() {
1023 return None;
1024 }
1025 Some([frame[base], frame[base + 1], frame[base + 2]])
1026 }
1027 pub fn to_bytes(&self) -> Vec<u8> {
1034 let mut out = Vec::new();
1035 let n_atoms = self.n_atoms as u32;
1036 let n_frames = self.frames.len() as u32;
1037 out.extend_from_slice(&n_atoms.to_le_bytes());
1038 out.extend_from_slice(&n_frames.to_le_bytes());
1039 for frame in &self.frames {
1040 let record_len = (frame.len() * 4) as u32;
1041 out.extend_from_slice(&record_len.to_le_bytes());
1042 for &v in frame {
1043 out.extend_from_slice(&v.to_le_bytes());
1044 }
1045 }
1046 out
1047 }
1048 pub fn from_bytes(data: &[u8]) -> std::result::Result<Self, String> {
1051 if data.len() < 8 {
1052 return Err("DCD: too short".into());
1053 }
1054 let n_atoms = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
1055 let n_frames = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
1056 let mut dcd = AmberDcd::new(n_atoms);
1057 let mut offset = 8;
1058 for _ in 0..n_frames {
1059 if offset + 4 > data.len() {
1060 return Err("DCD: truncated frame record length".into());
1061 }
1062 let record_len = u32::from_le_bytes([
1063 data[offset],
1064 data[offset + 1],
1065 data[offset + 2],
1066 data[offset + 3],
1067 ]) as usize;
1068 offset += 4;
1069 if offset + record_len > data.len() {
1070 return Err("DCD: truncated frame data".into());
1071 }
1072 let n_floats = record_len / 4;
1073 let mut frame = Vec::with_capacity(n_floats);
1074 for i in 0..n_floats {
1075 let b = &data[offset + i * 4..offset + i * 4 + 4];
1076 frame.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
1077 }
1078 dcd.frames.push(frame);
1079 offset += record_len;
1080 }
1081 Ok(dcd)
1082 }
1083}