1use std::collections::HashMap;
34use std::ops::{Index, IndexMut};
35
36use slotmap::{SlotMap, new_key_type};
37
38use super::block::Block;
39use super::frame::Frame;
40use crate::error::MolRsError;
41use crate::types::{F, I, U};
42
43#[derive(Debug, Clone, PartialEq)]
49pub enum PropValue {
50 F64(f64),
51 Str(String),
52 Int(I),
53}
54
55impl From<f64> for PropValue {
56 fn from(v: f64) -> Self {
57 PropValue::F64(v)
58 }
59}
60impl From<I> for PropValue {
61 fn from(v: I) -> Self {
62 PropValue::Int(v)
63 }
64}
65impl From<&str> for PropValue {
66 fn from(v: &str) -> Self {
67 PropValue::Str(v.to_owned())
68 }
69}
70impl From<String> for PropValue {
71 fn from(v: String) -> Self {
72 PropValue::Str(v)
73 }
74}
75
76#[derive(Debug, Clone, Default, PartialEq)]
85pub struct Atom {
86 props: HashMap<String, PropValue>,
87}
88
89impl Atom {
90 pub fn new() -> Self {
92 Self::default()
93 }
94
95 pub fn xyz(symbol: &str, x: f64, y: f64, z: f64) -> Self {
97 let mut a = Self::new();
98 a.set("element", symbol);
99 a.set("x", x);
100 a.set("y", y);
101 a.set("z", z);
102 a
103 }
104
105 pub fn set(&mut self, key: &str, val: impl Into<PropValue>) {
109 self.props.insert(key.to_owned(), val.into());
110 }
111
112 pub fn get(&self, key: &str) -> Option<&PropValue> {
114 self.props.get(key)
115 }
116
117 pub fn get_mut(&mut self, key: &str) -> Option<&mut PropValue> {
119 self.props.get_mut(key)
120 }
121
122 pub fn get_f64(&self, key: &str) -> Option<f64> {
124 match self.props.get(key)? {
125 PropValue::F64(v) => Some(*v),
126 _ => None,
127 }
128 }
129
130 pub fn get_str(&self, key: &str) -> Option<&str> {
132 match self.props.get(key)? {
133 PropValue::Str(s) => Some(s.as_str()),
134 _ => None,
135 }
136 }
137
138 pub fn get_int(&self, key: &str) -> Option<I> {
140 match self.props.get(key)? {
141 PropValue::Int(v) => Some(*v),
142 _ => None,
143 }
144 }
145
146 pub fn contains_key(&self, key: &str) -> bool {
148 self.props.contains_key(key)
149 }
150
151 pub fn remove(&mut self, key: &str) -> Option<PropValue> {
153 self.props.remove(key)
154 }
155
156 pub fn keys(&self) -> impl Iterator<Item = &str> {
158 self.props.keys().map(|k| k.as_str())
159 }
160
161 pub fn len(&self) -> usize {
163 self.props.len()
164 }
165
166 pub fn is_empty(&self) -> bool {
168 self.props.is_empty()
169 }
170}
171
172impl Index<&str> for Atom {
173 type Output = PropValue;
174 fn index(&self, key: &str) -> &Self::Output {
175 self.props
176 .get(key)
177 .unwrap_or_else(|| panic!("Atom does not contain key '{}'", key))
178 }
179}
180
181impl IndexMut<&str> for Atom {
182 fn index_mut(&mut self, key: &str) -> &mut Self::Output {
183 self.props
184 .get_mut(key)
185 .unwrap_or_else(|| panic!("Atom does not contain key '{}'", key))
186 }
187}
188
189pub type Bead = Atom;
191
192new_key_type! {
197 pub struct AtomId;
199 pub struct BondId;
201 pub struct AngleId;
203 pub struct DihedralId;
205}
206
207#[derive(Debug, Clone)]
213pub struct Bond {
214 pub atoms: [AtomId; 2],
215 pub props: HashMap<String, PropValue>,
216}
217
218#[derive(Debug, Clone)]
220pub struct Angle {
221 pub atoms: [AtomId; 3],
222 pub props: HashMap<String, PropValue>,
223}
224
225#[derive(Debug, Clone)]
227pub struct Dihedral {
228 pub atoms: [AtomId; 4],
229 pub props: HashMap<String, PropValue>,
230}
231
232#[derive(Debug, Clone)]
239pub struct MolGraph {
240 atoms: SlotMap<AtomId, Atom>,
241 bonds: SlotMap<BondId, Bond>,
242 angles: SlotMap<AngleId, Angle>,
243 dihedrals: SlotMap<DihedralId, Dihedral>,
244 adjacency: HashMap<AtomId, Vec<BondId>>,
245}
246
247impl Default for MolGraph {
248 fn default() -> Self {
249 Self::new()
250 }
251}
252
253impl MolGraph {
254 pub fn new() -> Self {
256 Self {
257 atoms: SlotMap::with_key(),
258 bonds: SlotMap::with_key(),
259 angles: SlotMap::with_key(),
260 dihedrals: SlotMap::with_key(),
261 adjacency: HashMap::new(),
262 }
263 }
264
265 pub fn add_atom(&mut self, atom: Atom) -> AtomId {
271 let id = self.atoms.insert(atom);
272 self.adjacency.insert(id, Vec::new());
273 id
274 }
275
276 pub fn remove_atom(&mut self, id: AtomId) -> Result<Atom, MolRsError> {
278 let atom = self
279 .atoms
280 .remove(id)
281 .ok_or_else(|| MolRsError::not_found("atom", format!("AtomId {:?}", id)))?;
282
283 let incident: Vec<BondId> = self.adjacency.remove(&id).unwrap_or_default();
285 for bid in incident {
286 self.remove_bond_inner(bid, id);
287 }
288
289 let doomed_angles: Vec<AngleId> = self
291 .angles
292 .iter()
293 .filter(|(_, a)| a.atoms.contains(&id))
294 .map(|(aid, _)| aid)
295 .collect();
296 for aid in doomed_angles {
297 self.angles.remove(aid);
298 }
299
300 let doomed_dihedrals: Vec<DihedralId> = self
302 .dihedrals
303 .iter()
304 .filter(|(_, d)| d.atoms.contains(&id))
305 .map(|(did, _)| did)
306 .collect();
307 for did in doomed_dihedrals {
308 self.dihedrals.remove(did);
309 }
310
311 Ok(atom)
312 }
313
314 pub fn get_atom(&self, id: AtomId) -> Result<&Atom, MolRsError> {
316 self.atoms
317 .get(id)
318 .ok_or_else(|| MolRsError::not_found("atom", format!("AtomId {:?}", id)))
319 }
320
321 pub fn get_atom_mut(&mut self, id: AtomId) -> Result<&mut Atom, MolRsError> {
323 self.atoms
324 .get_mut(id)
325 .ok_or_else(|| MolRsError::not_found("atom", format!("AtomId {:?}", id)))
326 }
327
328 pub fn add_bond(&mut self, a: AtomId, b: AtomId) -> Result<BondId, MolRsError> {
334 if !self.atoms.contains_key(a) {
335 return Err(MolRsError::not_found("atom", format!("AtomId {:?}", a)));
336 }
337 if !self.atoms.contains_key(b) {
338 return Err(MolRsError::not_found("atom", format!("AtomId {:?}", b)));
339 }
340 let bond = Bond {
341 atoms: [a, b],
342 props: HashMap::new(),
343 };
344 let bid = self.bonds.insert(bond);
345 self.adjacency.entry(a).or_default().push(bid);
346 self.adjacency.entry(b).or_default().push(bid);
347 Ok(bid)
348 }
349
350 pub fn remove_bond(&mut self, id: BondId) -> Result<Bond, MolRsError> {
352 let bond = self
353 .bonds
354 .remove(id)
355 .ok_or_else(|| MolRsError::not_found("bond", format!("BondId {:?}", id)))?;
356 for &aid in &bond.atoms {
357 if let Some(adj) = self.adjacency.get_mut(&aid) {
358 adj.retain(|bid| *bid != id);
359 }
360 }
361 Ok(bond)
362 }
363
364 pub fn get_bond(&self, id: BondId) -> Result<&Bond, MolRsError> {
366 self.bonds
367 .get(id)
368 .ok_or_else(|| MolRsError::not_found("bond", format!("BondId {:?}", id)))
369 }
370
371 pub fn get_bond_mut(&mut self, id: BondId) -> Result<&mut Bond, MolRsError> {
373 self.bonds
374 .get_mut(id)
375 .ok_or_else(|| MolRsError::not_found("bond", format!("BondId {:?}", id)))
376 }
377
378 fn remove_bond_inner(&mut self, bid: BondId, removed_atom: AtomId) {
380 if let Some(bond) = self.bonds.remove(bid) {
381 for &aid in &bond.atoms {
384 if aid != removed_atom
385 && let Some(adj) = self.adjacency.get_mut(&aid)
386 {
387 adj.retain(|b| *b != bid);
388 }
389 }
390 }
391 }
392
393 pub fn add_angle(&mut self, i: AtomId, j: AtomId, k: AtomId) -> Result<AngleId, MolRsError> {
399 for &atom_id in &[i, j, k] {
400 if !self.atoms.contains_key(atom_id) {
401 return Err(MolRsError::not_found(
402 "atom",
403 format!("AtomId {:?}", atom_id),
404 ));
405 }
406 }
407 let angle = Angle {
408 atoms: [i, j, k],
409 props: HashMap::new(),
410 };
411 Ok(self.angles.insert(angle))
412 }
413
414 pub fn remove_angle(&mut self, id: AngleId) -> Result<Angle, MolRsError> {
416 self.angles
417 .remove(id)
418 .ok_or_else(|| MolRsError::not_found("angle", format!("AngleId {:?}", id)))
419 }
420
421 pub fn get_angle(&self, id: AngleId) -> Result<&Angle, MolRsError> {
423 self.angles
424 .get(id)
425 .ok_or_else(|| MolRsError::not_found("angle", format!("AngleId {:?}", id)))
426 }
427
428 pub fn add_dihedral(
434 &mut self,
435 i: AtomId,
436 j: AtomId,
437 k: AtomId,
438 l: AtomId,
439 ) -> Result<DihedralId, MolRsError> {
440 for &atom_id in &[i, j, k, l] {
441 if !self.atoms.contains_key(atom_id) {
442 return Err(MolRsError::not_found(
443 "atom",
444 format!("AtomId {:?}", atom_id),
445 ));
446 }
447 }
448 let dih = Dihedral {
449 atoms: [i, j, k, l],
450 props: HashMap::new(),
451 };
452 Ok(self.dihedrals.insert(dih))
453 }
454
455 pub fn remove_dihedral(&mut self, id: DihedralId) -> Result<Dihedral, MolRsError> {
457 self.dihedrals
458 .remove(id)
459 .ok_or_else(|| MolRsError::not_found("dihedral", format!("DihedralId {:?}", id)))
460 }
461
462 pub fn get_dihedral(&self, id: DihedralId) -> Result<&Dihedral, MolRsError> {
464 self.dihedrals
465 .get(id)
466 .ok_or_else(|| MolRsError::not_found("dihedral", format!("DihedralId {:?}", id)))
467 }
468
469 pub fn atoms(&self) -> impl Iterator<Item = (AtomId, &Atom)> {
475 self.atoms.iter()
476 }
477
478 pub fn bonds(&self) -> impl Iterator<Item = (BondId, &Bond)> {
480 self.bonds.iter()
481 }
482
483 pub fn angles(&self) -> impl Iterator<Item = (AngleId, &Angle)> {
485 self.angles.iter()
486 }
487
488 pub fn dihedrals(&self) -> impl Iterator<Item = (DihedralId, &Dihedral)> {
490 self.dihedrals.iter()
491 }
492
493 pub fn n_atoms(&self) -> usize {
495 self.atoms.len()
496 }
497 pub fn n_bonds(&self) -> usize {
499 self.bonds.len()
500 }
501 pub fn n_angles(&self) -> usize {
503 self.angles.len()
504 }
505 pub fn n_dihedrals(&self) -> usize {
507 self.dihedrals.len()
508 }
509
510 pub fn neighbors(&self, id: AtomId) -> impl Iterator<Item = AtomId> + '_ {
512 self.adjacency
513 .get(&id)
514 .into_iter()
515 .flat_map(|bonds| bonds.iter())
516 .filter_map(move |&bid| {
517 let bond = self.bonds.get(bid)?;
518 let other = if bond.atoms[0] == id {
519 bond.atoms[1]
520 } else {
521 bond.atoms[0]
522 };
523 Some(other)
524 })
525 }
526
527 pub fn neighbor_bonds(&self, id: AtomId) -> impl Iterator<Item = (AtomId, f64)> + '_ {
531 self.adjacency
532 .get(&id)
533 .into_iter()
534 .flat_map(|bonds| bonds.iter())
535 .filter_map(move |&bid| {
536 let bond = self.bonds.get(bid)?;
537 let other = if bond.atoms[0] == id {
538 bond.atoms[1]
539 } else {
540 bond.atoms[0]
541 };
542 let order = match bond.props.get("order") {
543 Some(PropValue::F64(v)) => *v,
544 _ => 1.0,
545 };
546 Some((other, order))
547 })
548 }
549
550 pub fn translate(&mut self, delta: [f64; 3]) {
556 for (_, atom) in self.atoms.iter_mut() {
557 let keys = ["x", "y", "z"];
558 for (i, key) in keys.iter().enumerate() {
559 if let Some(PropValue::F64(v)) = atom.get_mut(key) {
560 *v += delta[i];
561 }
562 }
563 }
564 }
565
566 pub fn rotate(&mut self, axis: [f64; 3], angle: f64, about: Option<[f64; 3]>) {
569 let len = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
571 if len < 1e-15 {
572 return;
573 }
574 let k = [axis[0] / len, axis[1] / len, axis[2] / len];
575 let cos_a = angle.cos();
576 let sin_a = angle.sin();
577 let origin = about.unwrap_or([0.0, 0.0, 0.0]);
578
579 for (_, atom) in self.atoms.iter_mut() {
580 let (Some(PropValue::F64(x)), Some(PropValue::F64(y)), Some(PropValue::F64(z))) =
581 (atom.get("x"), atom.get("y"), atom.get("z"))
582 else {
583 continue;
584 };
585 let p = [x - origin[0], y - origin[1], z - origin[2]];
586
587 let kdotp = k[0] * p[0] + k[1] * p[1] + k[2] * p[2];
589 let cross = [
590 k[1] * p[2] - k[2] * p[1],
591 k[2] * p[0] - k[0] * p[2],
592 k[0] * p[1] - k[1] * p[0],
593 ];
594 let rotated = [
595 p[0] * cos_a + cross[0] * sin_a + k[0] * kdotp * (1.0 - cos_a) + origin[0],
596 p[1] * cos_a + cross[1] * sin_a + k[1] * kdotp * (1.0 - cos_a) + origin[1],
597 p[2] * cos_a + cross[2] * sin_a + k[2] * kdotp * (1.0 - cos_a) + origin[2],
598 ];
599
600 atom.set("x", rotated[0]);
601 atom.set("y", rotated[1]);
602 atom.set("z", rotated[2]);
603 }
604 }
605
606 pub fn merge(&mut self, other: MolGraph) {
613 let mut atom_map: HashMap<AtomId, AtomId> = HashMap::new();
614
615 for (old_id, atom) in other.atoms {
617 let new_id = self.add_atom(atom);
618 atom_map.insert(old_id, new_id);
619 }
620
621 for (_, mut bond) in other.bonds {
623 let a = atom_map[&bond.atoms[0]];
624 let b = atom_map[&bond.atoms[1]];
625 bond.atoms = [a, b];
626 let bid = self.bonds.insert(bond);
627 self.adjacency.entry(a).or_default().push(bid);
628 self.adjacency.entry(b).or_default().push(bid);
629 }
630
631 for (_, mut angle) in other.angles {
633 angle.atoms = [
634 atom_map[&angle.atoms[0]],
635 atom_map[&angle.atoms[1]],
636 atom_map[&angle.atoms[2]],
637 ];
638 self.angles.insert(angle);
639 }
640
641 for (_, mut dih) in other.dihedrals {
643 dih.atoms = [
644 atom_map[&dih.atoms[0]],
645 atom_map[&dih.atoms[1]],
646 atom_map[&dih.atoms[2]],
647 atom_map[&dih.atoms[3]],
648 ];
649 self.dihedrals.insert(dih);
650 }
651 }
652
653 pub fn to_frame(&self) -> Frame {
661 use ndarray::Array1;
662
663 let mut frame = Frame::new();
664
665 let atom_ids: Vec<AtomId> = self.atoms.keys().collect();
667 let n = atom_ids.len();
668 let id_to_row: HashMap<AtomId, usize> = atom_ids
669 .iter()
670 .enumerate()
671 .map(|(i, &id)| (id, i))
672 .collect();
673
674 let mut all_keys: Vec<String> = {
676 let mut set = std::collections::BTreeSet::new();
677 for (_, atom) in &self.atoms {
678 for k in atom.keys() {
679 set.insert(k.to_owned());
680 }
681 }
682 set.into_iter().collect()
683 };
684 all_keys.sort();
685
686 let mut atoms_block = Block::new();
688 for key in &all_keys {
689 let first_val = atom_ids
691 .iter()
692 .filter_map(|&id| self.atoms.get(id).and_then(|a| a.get(key)))
693 .next();
694
695 match first_val {
696 Some(PropValue::F64(_)) => {
697 let col: Vec<F> = atom_ids
698 .iter()
699 .map(|&id| {
700 self.atoms
701 .get(id)
702 .and_then(|a| a.get_f64(key))
703 .unwrap_or(0.0) as F
704 })
705 .collect();
706 let _ = atoms_block.insert(key.as_str(), Array1::from_vec(col).into_dyn());
707 }
708 Some(PropValue::Int(_)) => {
709 let col: Vec<I> = atom_ids
710 .iter()
711 .map(|&id| self.atoms.get(id).and_then(|a| a.get_int(key)).unwrap_or(0))
712 .collect();
713 let _ = atoms_block.insert(key.as_str(), Array1::from_vec(col).into_dyn());
714 }
715 Some(PropValue::Str(_)) => {
716 let col: Vec<String> = atom_ids
717 .iter()
718 .map(|&id| {
719 self.atoms
720 .get(id)
721 .and_then(|a| a.get_str(key))
722 .unwrap_or("")
723 .to_owned()
724 })
725 .collect();
726 let _ = atoms_block.insert(key.as_str(), Array1::from_vec(col).into_dyn());
727 }
728 _ => {}
729 }
730 }
731 if n > 0 {
732 frame.insert("atoms", atoms_block);
733 }
734
735 if !self.bonds.is_empty() {
737 let mut bonds_block = Block::new();
738 let mut col_i: Vec<U> = Vec::with_capacity(self.bonds.len());
739 let mut col_j: Vec<U> = Vec::with_capacity(self.bonds.len());
740 let mut col_order: Vec<F> = Vec::with_capacity(self.bonds.len());
741 for (_, bond) in &self.bonds {
742 col_i.push(id_to_row[&bond.atoms[0]] as U);
743 col_j.push(id_to_row[&bond.atoms[1]] as U);
744 let order = bond
745 .props
746 .get("order")
747 .and_then(|v| {
748 if let PropValue::F64(f) = v {
749 Some(*f as F)
750 } else {
751 None
752 }
753 })
754 .unwrap_or(1.0);
755 col_order.push(order);
756 }
757 let _ = bonds_block.insert("atomi", Array1::from_vec(col_i).into_dyn());
758 let _ = bonds_block.insert("atomj", Array1::from_vec(col_j).into_dyn());
759 let _ = bonds_block.insert("order", Array1::from_vec(col_order).into_dyn());
760 frame.insert("bonds", bonds_block);
761 }
762
763 if !self.angles.is_empty() {
765 let mut angles_block = Block::new();
766 let mut ci: Vec<U> = Vec::with_capacity(self.angles.len());
767 let mut cj: Vec<U> = Vec::with_capacity(self.angles.len());
768 let mut ck: Vec<U> = Vec::with_capacity(self.angles.len());
769 for (_, angle) in &self.angles {
770 ci.push(id_to_row[&angle.atoms[0]] as U);
771 cj.push(id_to_row[&angle.atoms[1]] as U);
772 ck.push(id_to_row[&angle.atoms[2]] as U);
773 }
774 let _ = angles_block.insert("atomi", Array1::from_vec(ci).into_dyn());
775 let _ = angles_block.insert("atomj", Array1::from_vec(cj).into_dyn());
776 let _ = angles_block.insert("atomk", Array1::from_vec(ck).into_dyn());
777 frame.insert("angles", angles_block);
778 }
779
780 if !self.dihedrals.is_empty() {
782 let mut dih_block = Block::new();
783 let mut ci: Vec<U> = Vec::with_capacity(self.dihedrals.len());
784 let mut cj: Vec<U> = Vec::with_capacity(self.dihedrals.len());
785 let mut ck: Vec<U> = Vec::with_capacity(self.dihedrals.len());
786 let mut cl: Vec<U> = Vec::with_capacity(self.dihedrals.len());
787 for (_, d) in &self.dihedrals {
788 ci.push(id_to_row[&d.atoms[0]] as U);
789 cj.push(id_to_row[&d.atoms[1]] as U);
790 ck.push(id_to_row[&d.atoms[2]] as U);
791 cl.push(id_to_row[&d.atoms[3]] as U);
792 }
793 let _ = dih_block.insert("atomi", Array1::from_vec(ci).into_dyn());
794 let _ = dih_block.insert("atomj", Array1::from_vec(cj).into_dyn());
795 let _ = dih_block.insert("atomk", Array1::from_vec(ck).into_dyn());
796 let _ = dih_block.insert("atoml", Array1::from_vec(cl).into_dyn());
797 frame.insert("dihedrals", dih_block);
798 }
799
800 frame
801 }
802
803 pub fn from_frame(frame: &Frame) -> Result<Self, MolRsError> {
806 let mut g = MolGraph::new();
807
808 let atoms_block = frame
809 .get("atoms")
810 .ok_or_else(|| MolRsError::parse("Frame missing 'atoms' block"))?;
811
812 let nrows = atoms_block.nrows().unwrap_or(0);
813
814 let col_keys: Vec<String> = atoms_block.keys().map(|k| k.to_owned()).collect();
816
817 let mut float_cols: Vec<(&str, &ndarray::ArrayD<F>)> = Vec::new();
819 let mut i64_cols: Vec<(&str, &ndarray::ArrayD<I>)> = Vec::new();
820 let mut str_cols: Vec<(&str, &ndarray::ArrayD<String>)> = Vec::new();
821
822 for key in &col_keys {
823 if let Some(arr) = atoms_block.get_float(key) {
824 float_cols.push((key.as_str(), arr));
825 } else if let Some(arr) = atoms_block.get_int(key) {
826 i64_cols.push((key.as_str(), arr));
827 } else if let Some(arr) = atoms_block.get_string(key) {
828 str_cols.push((key.as_str(), arr));
829 }
830 }
831
832 let mut atom_ids: Vec<AtomId> = Vec::with_capacity(nrows);
834 for row in 0..nrows {
835 let mut atom = Atom::new();
836 for &(key, arr) in &float_cols {
837 #[allow(clippy::unnecessary_cast)]
838 atom.set(key, arr[[row]] as f64);
839 }
840 for &(key, arr) in &i64_cols {
841 atom.set(key, PropValue::Int(arr[[row]]));
842 }
843 for &(key, arr) in &str_cols {
844 atom.set(key, PropValue::Str(arr[[row]].clone()));
845 }
846 atom_ids.push(g.add_atom(atom));
847 }
848
849 if let Some(bonds_block) = frame.get("bonds") {
851 let col_i = bonds_block
852 .get_uint("atomi")
853 .ok_or_else(|| MolRsError::parse("bonds block missing 'atomi' column"))?;
854 let col_j = bonds_block
855 .get_uint("atomj")
856 .ok_or_else(|| MolRsError::parse("bonds block missing 'atomj' column"))?;
857
858 let col_order_f = bonds_block.get_float("order");
860 let col_order_u = bonds_block.get_uint("order");
861
862 let nb = bonds_block.nrows().unwrap_or(0);
863 for row in 0..nb {
864 let ai = col_i[[row]] as usize;
865 let aj = col_j[[row]] as usize;
866 if ai < atom_ids.len() && aj < atom_ids.len() {
867 let bid = g.add_bond(atom_ids[ai], atom_ids[aj])?;
868 #[allow(clippy::unnecessary_cast)]
869 let order = col_order_f
870 .map(|c| c[[row]] as f64)
871 .or_else(|| col_order_u.map(|c| c[[row]] as f64));
872 if let Some(o) = order
873 && let Ok(bond) = g.get_bond_mut(bid)
874 {
875 bond.props.insert("order".to_string(), PropValue::F64(o));
876 }
877 }
878 }
879 }
880
881 if let Some(angles_block) = frame.get("angles")
883 && let (Some(ci), Some(cj), Some(ck)) = (
884 angles_block.get_uint("atomi"),
885 angles_block.get_uint("atomj"),
886 angles_block.get_uint("atomk"),
887 )
888 {
889 let na = angles_block.nrows().unwrap_or(0);
890 for row in 0..na {
891 let ai = ci[[row]] as usize;
892 let aj = cj[[row]] as usize;
893 let ak = ck[[row]] as usize;
894 if ai < atom_ids.len() && aj < atom_ids.len() && ak < atom_ids.len() {
895 g.add_angle(atom_ids[ai], atom_ids[aj], atom_ids[ak])?;
896 }
897 }
898 }
899
900 if let Some(dih_block) = frame.get("dihedrals")
902 && let (Some(ci), Some(cj), Some(ck), Some(cl)) = (
903 dih_block.get_uint("atomi"),
904 dih_block.get_uint("atomj"),
905 dih_block.get_uint("atomk"),
906 dih_block.get_uint("atoml"),
907 )
908 {
909 let nd = dih_block.nrows().unwrap_or(0);
910 for row in 0..nd {
911 let ai = ci[[row]] as usize;
912 let aj = cj[[row]] as usize;
913 let ak = ck[[row]] as usize;
914 let al = cl[[row]] as usize;
915 if ai < atom_ids.len()
916 && aj < atom_ids.len()
917 && ak < atom_ids.len()
918 && al < atom_ids.len()
919 {
920 g.add_dihedral(atom_ids[ai], atom_ids[aj], atom_ids[ak], atom_ids[al])?;
921 }
922 }
923 }
924
925 Ok(g)
926 }
927}
928
929#[cfg(test)]
934mod tests {
935 use super::*;
936
937 #[test]
940 fn test_propvalue_from() {
941 let v: PropValue = std::f64::consts::PI.into();
942 assert_eq!(v, PropValue::F64(std::f64::consts::PI));
943
944 let v: PropValue = (42 as I).into();
945 assert_eq!(v, PropValue::Int(42 as I));
946
947 let v: PropValue = "H".into();
948 assert_eq!(v, PropValue::Str("H".to_owned()));
949 }
950
951 #[test]
952 fn test_atom_dict_api() {
953 let mut a = Atom::new();
954 a.set("x", 1.5);
955 a.set("element", "C");
956 a.set("type_id", PropValue::Int(3 as I));
957
958 assert_eq!(a.get_f64("x"), Some(1.5));
959 assert_eq!(a.get_str("element"), Some("C"));
960 assert_eq!(a.get_int("type_id"), Some(3 as I));
961 assert_eq!(a.get_f64("missing"), None);
962 assert!(a.contains_key("x"));
963 assert!(!a.contains_key("missing"));
964 assert_eq!(a.len(), 3);
965
966 a.remove("type_id");
967 assert_eq!(a.len(), 2);
968 }
969
970 #[test]
971 fn test_atom_index() {
972 let mut a = Atom::xyz("O", 1.0, 2.0, 3.0);
973 assert_eq!(a["x"], PropValue::F64(1.0));
974
975 a["x"] = PropValue::F64(99.0);
977 assert_eq!(a.get_f64("x"), Some(99.0));
978 }
979
980 #[test]
981 fn test_atom_xyz_constructor() {
982 let a = Atom::xyz("H", 0.96, 0.0, 0.0);
983 assert_eq!(a.get_str("element"), Some("H"));
984 assert_eq!(a.get_f64("x"), Some(0.96));
985 assert_eq!(a.get_f64("y"), Some(0.0));
986 assert_eq!(a.get_f64("z"), Some(0.0));
987 }
988
989 #[test]
992 fn test_add_remove_atom() {
993 let mut g = MolGraph::new();
994 assert_eq!(g.n_atoms(), 0);
995
996 let id = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
997 assert_eq!(g.n_atoms(), 1);
998 assert_eq!(
999 g.get_atom(id).expect("get atom").get_str("element"),
1000 Some("C")
1001 );
1002
1003 g.remove_atom(id).expect("remove atom");
1004 assert_eq!(g.n_atoms(), 0);
1005 assert!(g.get_atom(id).is_err());
1006 }
1007
1008 #[test]
1009 fn test_atom_mut() {
1010 let mut g = MolGraph::new();
1011 let id = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
1012
1013 g.get_atom_mut(id).expect("get atom mut").set("x", 5.0);
1014 assert_eq!(g.get_atom(id).expect("get atom").get_f64("x"), Some(5.0));
1015 }
1016
1017 #[test]
1020 fn test_add_remove_bond() {
1021 let mut g = MolGraph::new();
1022 let a = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
1023 let b = g.add_atom(Atom::xyz("O", 1.0, 0.0, 0.0));
1024
1025 let bid = g.add_bond(a, b).expect("add bond");
1026 assert_eq!(g.n_bonds(), 1);
1027
1028 let neigh_a: Vec<AtomId> = g.neighbors(a).collect();
1030 assert_eq!(neigh_a, vec![b]);
1031 let neigh_b: Vec<AtomId> = g.neighbors(b).collect();
1032 assert_eq!(neigh_b, vec![a]);
1033
1034 g.remove_bond(bid).expect("remove bond");
1036 assert_eq!(g.n_bonds(), 0);
1037 assert_eq!(g.neighbors(a).count(), 0);
1038 assert_eq!(g.neighbors(b).count(), 0);
1039 }
1040
1041 #[test]
1042 fn test_bond_invalid_atoms() {
1043 let mut g = MolGraph::new();
1044 let a = g.add_atom(Atom::new());
1045 let b = g.add_atom(Atom::new());
1046 g.remove_atom(b).expect("remove atom b");
1048 assert!(g.add_bond(a, b).is_err());
1049 }
1050
1051 #[test]
1054 fn test_cascading_deletion() {
1055 let mut g = MolGraph::new();
1056 let a = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
1057 let b = g.add_atom(Atom::xyz("H", 1.0, 0.0, 0.0));
1058 let c = g.add_atom(Atom::xyz("H", -1.0, 0.0, 0.0));
1059 let d = g.add_atom(Atom::xyz("H", 0.0, 1.0, 0.0));
1060
1061 g.add_bond(a, b).expect("add bond");
1062 g.add_bond(a, c).expect("add bond");
1063 g.add_bond(a, d).expect("add bond");
1064 g.add_angle(b, a, c).expect("add angle");
1065 g.add_dihedral(b, a, c, d).expect("add dihedral");
1066
1067 assert_eq!(g.n_bonds(), 3);
1068 assert_eq!(g.n_angles(), 1);
1069 assert_eq!(g.n_dihedrals(), 1);
1070
1071 g.remove_atom(a).expect("remove atom a");
1073 assert_eq!(g.n_atoms(), 3);
1074 assert_eq!(g.n_bonds(), 0);
1075 assert_eq!(g.n_angles(), 0);
1076 assert_eq!(g.n_dihedrals(), 0);
1077 }
1078
1079 #[test]
1082 fn test_angle_crud() {
1083 let mut g = MolGraph::new();
1084 let a = g.add_atom(Atom::new());
1085 let b = g.add_atom(Atom::new());
1086 let c = g.add_atom(Atom::new());
1087
1088 let aid = g.add_angle(a, b, c).expect("add angle");
1089 assert_eq!(g.n_angles(), 1);
1090 assert_eq!(g.get_angle(aid).expect("get angle").atoms, [a, b, c]);
1091
1092 g.remove_angle(aid).expect("remove angle");
1093 assert_eq!(g.n_angles(), 0);
1094 }
1095
1096 #[test]
1097 fn test_dihedral_crud() {
1098 let mut g = MolGraph::new();
1099 let a = g.add_atom(Atom::new());
1100 let b = g.add_atom(Atom::new());
1101 let c = g.add_atom(Atom::new());
1102 let d = g.add_atom(Atom::new());
1103
1104 let did = g.add_dihedral(a, b, c, d).expect("add dihedral");
1105 assert_eq!(g.n_dihedrals(), 1);
1106 assert_eq!(
1107 g.get_dihedral(did).expect("get dihedral").atoms,
1108 [a, b, c, d]
1109 );
1110
1111 g.remove_dihedral(did).expect("remove dihedral");
1112 assert_eq!(g.n_dihedrals(), 0);
1113 }
1114
1115 #[test]
1118 fn test_iteration_counts() {
1119 let mut g = MolGraph::new();
1120 let a = g.add_atom(Atom::xyz("H", 0.0, 0.0, 0.0));
1121 let b = g.add_atom(Atom::xyz("O", 1.0, 0.0, 0.0));
1122 let c = g.add_atom(Atom::xyz("H", 2.0, 0.0, 0.0));
1123
1124 g.add_bond(a, b).expect("add bond");
1125 g.add_bond(b, c).expect("add bond");
1126 g.add_angle(a, b, c).expect("add angle");
1127
1128 assert_eq!(g.atoms().count(), 3);
1129 assert_eq!(g.bonds().count(), 2);
1130 assert_eq!(g.angles().count(), 1);
1131 assert_eq!(g.dihedrals().count(), 0);
1132 }
1133
1134 #[test]
1137 fn test_neighbors() {
1138 let mut g = MolGraph::new();
1139 let a = g.add_atom(Atom::new());
1140 let b = g.add_atom(Atom::new());
1141 let c = g.add_atom(Atom::new());
1142
1143 g.add_bond(a, b).expect("add bond");
1144 g.add_bond(a, c).expect("add bond");
1145
1146 let mut n: Vec<AtomId> = g.neighbors(a).collect();
1147 n.sort_by_key(|id| id.0); assert_eq!(n.len(), 2);
1149 assert!(n.contains(&b));
1150 assert!(n.contains(&c));
1151
1152 let nb: Vec<AtomId> = g.neighbors(b).collect();
1154 assert_eq!(nb, vec![a]);
1155 }
1156
1157 #[test]
1160 fn test_translate() {
1161 let mut g = MolGraph::new();
1162 let id = g.add_atom(Atom::xyz("C", 1.0, 2.0, 3.0));
1163
1164 g.translate([10.0, 20.0, 30.0]);
1165
1166 let a = g.get_atom(id).expect("get atom");
1167 assert!((a.get_f64("x").unwrap() - 11.0).abs() < 1e-12);
1168 assert!((a.get_f64("y").unwrap() - 22.0).abs() < 1e-12);
1169 assert!((a.get_f64("z").unwrap() - 33.0).abs() < 1e-12);
1170 }
1171
1172 #[test]
1173 fn test_translate_skips_missing_xyz() {
1174 let mut g = MolGraph::new();
1175 let id = g.add_atom(Atom::new()); g.translate([1.0, 2.0, 3.0]);
1177 assert!(g.get_atom(id).expect("get atom").get_f64("x").is_none());
1179 }
1180
1181 #[test]
1182 fn test_rotate_90_deg_z() {
1183 let mut g = MolGraph::new();
1184 let id = g.add_atom(Atom::xyz("C", 1.0, 0.0, 0.0));
1185
1186 let half_pi = std::f64::consts::FRAC_PI_2;
1187 g.rotate([0.0, 0.0, 1.0], half_pi, None);
1188
1189 let a = g.get_atom(id).expect("get atom");
1190 assert!((a.get_f64("x").unwrap()).abs() < 1e-12);
1191 assert!((a.get_f64("y").unwrap() - 1.0).abs() < 1e-12);
1192 assert!((a.get_f64("z").unwrap()).abs() < 1e-12);
1193 }
1194
1195 #[test]
1198 fn test_to_from_frame() {
1199 let mut g = MolGraph::new();
1200 let o = g.add_atom(Atom::xyz("O", 0.0, 0.0, 0.0));
1201 let h1 = g.add_atom(Atom::xyz("H", 0.96, 0.0, 0.0));
1202 let h2 = g.add_atom(Atom::xyz("H", -0.24, 0.93, 0.0));
1203
1204 g.add_bond(o, h1).expect("add bond");
1205 g.add_bond(o, h2).expect("add bond");
1206 g.add_angle(h1, o, h2).expect("add angle");
1207
1208 let frame = g.to_frame();
1209 assert!(frame.contains_key("atoms"));
1210 assert!(frame.contains_key("bonds"));
1211 assert!(frame.contains_key("angles"));
1212
1213 assert_eq!(frame["atoms"].nrows(), Some(3));
1214 assert_eq!(frame["bonds"].nrows(), Some(2));
1215 assert_eq!(frame["angles"].nrows(), Some(1));
1216
1217 let g2 = MolGraph::from_frame(&frame).expect("from_frame");
1219 assert_eq!(g2.n_atoms(), 3);
1220 assert_eq!(g2.n_bonds(), 2);
1221 assert_eq!(g2.n_angles(), 1);
1222 }
1223
1224 #[test]
1227 fn test_merge() {
1228 let mut g1 = MolGraph::new();
1229 let a = g1.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
1230 let b = g1.add_atom(Atom::xyz("O", 1.0, 0.0, 0.0));
1231 g1.add_bond(a, b).expect("add bond");
1232
1233 let mut g2 = MolGraph::new();
1234 let c = g2.add_atom(Atom::xyz("N", 5.0, 0.0, 0.0));
1235 let d = g2.add_atom(Atom::xyz("H", 6.0, 0.0, 0.0));
1236 g2.add_bond(c, d).expect("add bond");
1237
1238 g1.merge(g2);
1239
1240 assert_eq!(g1.n_atoms(), 4);
1241 assert_eq!(g1.n_bonds(), 2);
1242 }
1243
1244 #[test]
1247 fn test_clone_independence() {
1248 let mut g = MolGraph::new();
1249 let id = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
1250 g.add_atom(Atom::xyz("H", 1.0, 0.0, 0.0));
1251
1252 let mut g2 = g.clone();
1253 g.get_atom_mut(id).expect("get atom mut").set("x", 99.0);
1255
1256 assert_eq!(g2.get_atom(id).expect("get atom").get_f64("x"), Some(0.0));
1258
1259 assert_eq!(g2.n_atoms(), 2);
1261 let first_id = {
1262 let (fid, _) = g2.atoms().next().unwrap();
1263 fid
1264 };
1265 g2.remove_atom(first_id).expect("remove atom");
1266 assert_eq!(g2.n_atoms(), 1);
1267 assert_eq!(g.n_atoms(), 2);
1269 }
1270
1271 #[test]
1274 fn test_water_molecule() {
1275 let mut water = MolGraph::new();
1276 let o = water.add_atom(Atom::xyz("O", 0.0, 0.0, 0.0));
1277 let h1 = water.add_atom(Atom::xyz("H", 0.9572, 0.0, 0.0));
1278 let h2 = water.add_atom(Atom::xyz("H", -0.2399, 0.9266, 0.0));
1279
1280 water.add_bond(o, h1).expect("add bond");
1281 water.add_bond(o, h2).expect("add bond");
1282 water.add_angle(h1, o, h2).expect("add angle");
1283
1284 assert_eq!(water.n_atoms(), 3);
1285 assert_eq!(water.n_bonds(), 2);
1286 assert_eq!(water.n_angles(), 1);
1287
1288 assert_eq!(water.neighbors(o).count(), 2);
1290 assert_eq!(water.neighbors(h1).count(), 1);
1292 assert_eq!(water.neighbors(h2).count(), 1);
1293 }
1294
1295 #[test]
1298 fn test_coarse_grained() {
1299 let mut g = MolGraph::new();
1300
1301 let mut b1 = Bead::new();
1303 b1.set("name", "W");
1304 b1.set("x", 0.0);
1305 b1.set("y", 0.0);
1306 b1.set("z", 0.0);
1307 b1.set("mass", 72.0);
1308
1309 let mut b2 = Bead::new();
1310 b2.set("name", "W");
1311 b2.set("x", 4.7);
1312 b2.set("y", 0.0);
1313 b2.set("z", 0.0);
1314 b2.set("mass", 72.0);
1315
1316 let id1 = g.add_atom(b1);
1317 let id2 = g.add_atom(b2);
1318 g.add_bond(id1, id2).expect("add bond");
1319
1320 assert_eq!(g.n_atoms(), 2);
1321 assert_eq!(g.n_bonds(), 1);
1322 assert_eq!(
1323 g.get_atom(id1).expect("get atom").get_f64("mass"),
1324 Some(72.0)
1325 );
1326 assert_eq!(
1327 g.get_atom(id1).expect("get atom").get_str("name"),
1328 Some("W")
1329 );
1330 }
1331}