1use crate::atom::Atom;
4use crate::bond::{BondEntry, BondOrder};
5use crate::element::Element;
6use crate::stereo_group::StereoGroup;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct AtomIdx(pub u32);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct BondIdx(pub u32);
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum MolError {
19 InvalidAtomIdx(AtomIdx),
21 DuplicateBond(AtomIdx, AtomIdx),
23}
24
25impl core::fmt::Display for MolError {
26 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27 match self {
28 Self::InvalidAtomIdx(idx) => write!(f, "invalid atom index: {}", idx.0),
29 Self::DuplicateBond(a, b) => {
30 write!(f, "duplicate bond between atoms {} and {}", a.0, b.0)
31 }
32 }
33 }
34}
35
36impl std::error::Error for MolError {}
37
38pub const STEREO_H_SENTINEL: u32 = u32::MAX;
44
45pub struct Molecule {
46 atoms: Vec<Atom>,
47 bonds: Vec<BondEntry>,
48 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
50 stereo_groups: Vec<StereoGroup>,
52 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
60}
61
62impl Molecule {
63 pub fn atom_count(&self) -> usize {
65 self.atoms.len()
66 }
67
68 pub fn bond_count(&self) -> usize {
70 self.bonds.len()
71 }
72
73 pub fn atom(&self, idx: AtomIdx) -> &Atom {
78 let i = idx.0 as usize;
79 if i >= self.atoms.len() {
80 panic!("atom index {} out of range (molecule has {} atoms)", idx.0, self.atoms.len());
81 }
82 &self.atoms[i]
83 }
84
85 pub fn bond(&self, idx: BondIdx) -> &BondEntry {
87 let i = idx.0 as usize;
88 if i >= self.bonds.len() {
89 panic!("bond index {} out of range (molecule has {} bonds)", idx.0, self.bonds.len());
90 }
91 &self.bonds[i]
92 }
93
94 pub fn atoms(&self) -> impl Iterator<Item = (AtomIdx, &Atom)> {
96 self.atoms
97 .iter()
98 .enumerate()
99 .map(|(i, a)| (AtomIdx(i as u32), a))
100 }
101
102 pub fn bonds(&self) -> impl Iterator<Item = (BondIdx, &BondEntry)> {
104 self.bonds
105 .iter()
106 .enumerate()
107 .map(|(i, b)| (BondIdx(i as u32), b))
108 }
109
110 pub fn neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (AtomIdx, BondIdx)> + '_ {
112 let i = idx.0 as usize;
113 if i >= self.adjacency.len() {
114 panic!("atom index {} out of range (molecule has {} atoms)", idx.0, self.adjacency.len());
115 }
116 self.adjacency[i].iter().copied()
117 }
118
119 pub fn degree(&self, idx: AtomIdx) -> usize {
121 let i = idx.0 as usize;
122 if i >= self.adjacency.len() {
123 panic!("atom index {} out of range (molecule has {} atoms)", idx.0, self.adjacency.len());
124 }
125 self.adjacency[i].len()
126 }
127
128 pub fn bond_between(&self, a: AtomIdx, b: AtomIdx) -> Option<(BondIdx, &BondEntry)> {
130 let a_idx = a.0 as usize;
131 let b_idx = b.0 as usize;
132 if a_idx >= self.adjacency.len() || b_idx >= self.atoms.len() {
133 return None;
134 }
135 self.adjacency[a_idx]
136 .iter()
137 .find(|&&(nb, _)| nb == b)
138 .and_then(|&(_, bidx)| {
139 let bond_idx = bidx.0 as usize;
140 if bond_idx < self.bonds.len() {
141 Some((bidx, &self.bonds[bond_idx]))
142 } else {
143 None
144 }
145 })
146 }
147
148 pub fn formula(&self) -> String {
150 use std::collections::BTreeMap;
151 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
152 for (_, atom) in self.atoms() {
153 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
154 }
155
156 let mut result = String::new();
157 let push_count = |sym: &str, n: u32, out: &mut String| {
158 out.push_str(sym);
159 if n > 1 {
160 out.push_str(&n.to_string());
161 }
162 };
163
164 if let Some(c) = counts.remove("C") {
166 push_count("C", c, &mut result);
167 }
168 if let Some(h) = counts.remove("H") {
169 push_count("H", h, &mut result);
170 }
171 for (sym, count) in &counts {
172 push_count(sym, *count, &mut result);
173 }
174 result
175 }
176}
177
178impl Molecule {
183 pub fn with_atom_added(&self, atom: Atom) -> (Molecule, AtomIdx) {
186 let mut builder = MoleculeBuilder::new();
187 for (_, a) in self.atoms() {
188 builder.add_atom(a.clone());
189 }
190 for (_, b) in self.bonds() {
191 let _ = builder.add_bond(b.atom1, b.atom2, b.order);
192 }
193 builder.copy_stereo_from(self);
194 let new_idx = builder.add_atom(atom);
195 (builder.build(), new_idx)
196 }
197
198 pub fn with_bond_added(
204 &self,
205 a: AtomIdx,
206 b: AtomIdx,
207 order: BondOrder,
208 ) -> Result<(Molecule, BondIdx), MolError> {
209 let mut builder = MoleculeBuilder::new();
210 for (_, atom) in self.atoms() {
211 builder.add_atom(atom.clone());
212 }
213 for (_, bond) in self.bonds() {
214 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
215 }
216 builder.copy_stereo_from(self);
217 let bond_idx = builder.add_bond(a, b, order)?;
218 Ok((builder.build(), bond_idx))
219 }
220
221 pub fn with_atom_charge(&self, idx: AtomIdx, charge: i8) -> Molecule {
223 let mut builder = MoleculeBuilder::new();
224 for (aidx, atom) in self.atoms() {
225 let mut a = atom.clone();
226 if aidx == idx {
227 a.charge = charge;
228 }
229 builder.add_atom(a);
230 }
231 for (_, bond) in self.bonds() {
232 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
233 }
234 builder.copy_stereo_from(self);
235 builder.build()
236 }
237
238 pub fn with_atom_element(&self, idx: AtomIdx, el: Element) -> Molecule {
243 let mut builder = MoleculeBuilder::new();
244 for (aidx, atom) in self.atoms() {
245 let mut a = atom.clone();
246 if aidx == idx {
247 a.element = el;
248 a.chirality = crate::atom::Chirality::None;
250 a.hydrogen_count = None;
251 a.aromatic = false;
252 }
253 builder.add_atom(a);
254 }
255 for (_, bond) in self.bonds() {
256 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
257 }
258 builder.copy_stereo_from(self);
259 builder.clear_stereo_neighbor_order(idx);
261 builder.build()
262 }
263
264 pub fn with_atom_removed(&self, idx: AtomIdx) -> (Molecule, Vec<Option<AtomIdx>>) {
271 let n = self.atom_count();
272 let removed = idx.0 as usize;
273
274 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
276 let mut new_pos = 0u32;
277 for (old, slot) in remap.iter_mut().enumerate() {
278 if old == removed {
279 continue;
280 }
281 *slot = Some(AtomIdx(new_pos));
282 new_pos += 1;
283 }
284
285 let mut builder = MoleculeBuilder::new();
286 for (aidx, atom) in self.atoms() {
287 if aidx == idx {
288 continue;
289 }
290 builder.add_atom(atom.clone());
291 }
292 for (_, bond) in self.bonds() {
293 if bond.atom1 == idx || bond.atom2 == idx {
294 continue;
295 }
296 if let (Some(a1), Some(a2)) =
297 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
298 {
299 let _ = builder.add_bond(a1, a2, bond.order);
300 }
301 }
302 for (old_key, order) in &self.stereo_neighbor_order {
304 let old_atom = *old_key as usize;
305 if old_atom == removed {
306 continue; }
308 if let Some(Some(new_key)) = remap.get(old_atom) {
309 let new_order: Vec<u32> = order
310 .iter()
311 .filter_map(|&v| {
312 if v == STEREO_H_SENTINEL {
313 Some(STEREO_H_SENTINEL)
314 } else if v as usize == removed {
315 None } else {
317 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
318 }
319 })
320 .collect();
321 builder.set_stereo_neighbor_order(*new_key, new_order);
322 }
323 }
324 (builder.build(), remap)
325 }
326
327 pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
331 crate::valence::implicit_hcount(self, idx)
332 }
333
334 pub fn total_formula(&self) -> String {
340 use std::collections::BTreeMap;
341 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
342 let mut implicit_h: u32 = 0;
343 for (aidx, atom) in self.atoms() {
344 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
345 implicit_h += crate::valence::implicit_hcount(self, aidx) as u32;
346 }
347 *counts.entry("H").or_insert(0) += implicit_h;
348
349 let mut result = String::new();
350 let push_count = |sym: &str, n: u32, out: &mut String| {
351 out.push_str(sym);
352 if n > 1 {
353 out.push_str(&n.to_string());
354 }
355 };
356
357 if let Some(c) = counts.remove("C") {
358 push_count("C", c, &mut result);
359 }
360 if let Some(h) = counts.remove("H")
361 && h > 0
362 {
363 push_count("H", h, &mut result);
364 }
365 for (sym, count) in &counts {
366 push_count(sym, *count, &mut result);
367 }
368 result
369 }
370
371 pub fn formula_with_isotopes(&self) -> String {
377 use std::collections::BTreeMap;
378 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
380 let mut has_carbon = false;
381 let mut has_explicit_h = false;
382 for (_, atom) in self.atoms() {
383 let sym = atom.element.symbol();
384 let key = match atom.isotope {
385 Some(n) => format!("{n}{sym}"),
386 None => sym.to_string(),
387 };
388 if sym == "C" && atom.isotope.is_none() {
389 has_carbon = true;
390 }
391 if sym == "H" {
392 has_explicit_h = true;
393 }
394 *counts.entry(key).or_insert(0) += 1;
395 }
396
397 let push_count = |key: &str, n: u32, out: &mut String| {
398 out.push_str(key);
399 if n > 1 {
400 out.push_str(&n.to_string());
401 }
402 };
403
404 let mut result = String::new();
405 if has_carbon && let Some(c) = counts.remove("C") {
407 push_count("C", c, &mut result);
408 }
409 if has_explicit_h && let Some(h) = counts.remove("H") {
410 push_count("H", h, &mut result);
411 }
412 for (key, count) in &counts {
413 push_count(key, *count, &mut result);
414 }
415 result
416 }
417
418 pub fn with_atom_aromatic(&self, idx: AtomIdx, aromatic: bool) -> Molecule {
420 let mut builder = MoleculeBuilder::new();
421 for (aidx, atom) in self.atoms() {
422 let mut a = atom.clone();
423 if aidx == idx {
424 a.aromatic = aromatic;
425 }
426 builder.add_atom(a);
427 }
428 for (_, bond) in self.bonds() {
429 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
430 }
431 builder.copy_stereo_from(self);
432 builder.build()
433 }
434
435 pub fn with_bond_order(&self, idx: BondIdx, order: BondOrder) -> Molecule {
437 let mut builder = MoleculeBuilder::new();
438 for (_, atom) in self.atoms() {
439 builder.add_atom(atom.clone());
440 }
441 for (bidx, bond) in self.bonds() {
442 let o = if bidx == idx { order } else { bond.order };
443 let _ = builder.add_bond(bond.atom1, bond.atom2, o);
444 }
445 builder.copy_stereo_from(self);
446 builder.build()
447 }
448
449 pub fn with_bond_removed(&self, idx: BondIdx) -> Molecule {
453 let mut builder = MoleculeBuilder::new();
454 for (_, atom) in self.atoms() {
455 builder.add_atom(atom.clone());
456 }
457 for (bidx, bond) in self.bonds() {
458 if bidx == idx {
459 continue;
460 }
461 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
462 }
463 builder.copy_stereo_from(self);
464 builder.build()
465 }
466}
467
468impl Molecule {
473 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
475 let idx = AtomIdx(self.atoms.len() as u32);
476 self.atoms.push(atom);
477 self.adjacency.push(vec![]);
478 idx
479 }
480
481 pub fn remove_atom(&mut self, idx: AtomIdx) -> Vec<Option<AtomIdx>> {
487 let n = self.atoms.len();
488 let removed = idx.0 as usize;
489
490 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
491 let mut new_pos = 0u32;
492 for (old, slot) in remap.iter_mut().enumerate() {
493 if old == removed {
494 continue;
495 }
496 *slot = Some(AtomIdx(new_pos));
497 new_pos += 1;
498 }
499
500 self.atoms.remove(removed);
501
502 let mut new_bonds: Vec<BondEntry> = Vec::new();
504 for bond in &self.bonds {
505 if bond.atom1 == idx || bond.atom2 == idx {
506 continue;
507 }
508 if let (Some(a1), Some(a2)) =
509 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
510 {
511 new_bonds.push(BondEntry {
512 atom1: a1,
513 atom2: a2,
514 order: bond.order,
515 });
516 }
517 }
518 self.bonds = new_bonds;
519
520 let new_n = self.atoms.len();
522 self.adjacency = vec![vec![]; new_n];
523 for (bidx, bond) in self.bonds.iter().enumerate() {
524 let bi = BondIdx(bidx as u32);
525 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
526 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
527 }
528
529 let old_stereo = std::mem::take(&mut self.stereo_neighbor_order);
531 for (old_key, order) in old_stereo {
532 let old_atom = old_key as usize;
533 if old_atom == removed {
534 continue;
535 }
536 if let Some(Some(new_key)) = remap.get(old_atom) {
537 let new_order: Vec<u32> = order
538 .iter()
539 .filter_map(|&v| {
540 if v == STEREO_H_SENTINEL {
541 Some(STEREO_H_SENTINEL)
542 } else if v as usize == removed {
543 None
544 } else {
545 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
546 }
547 })
548 .collect();
549 self.stereo_neighbor_order.insert(new_key.0, new_order);
550 }
551 }
552
553 remap
554 }
555
556 pub fn add_bond(
560 &mut self,
561 a: AtomIdx,
562 b: AtomIdx,
563 order: BondOrder,
564 ) -> Result<BondIdx, MolError> {
565 let n = self.atoms.len() as u32;
566 if a.0 >= n {
567 return Err(MolError::InvalidAtomIdx(a));
568 }
569 if b.0 >= n {
570 return Err(MolError::InvalidAtomIdx(b));
571 }
572 if self.adjacency[a.0 as usize].iter().any(|&(nb, _)| nb == b) {
573 return Err(MolError::DuplicateBond(a, b));
574 }
575 let bidx = BondIdx(self.bonds.len() as u32);
576 self.bonds.push(BondEntry {
577 atom1: a,
578 atom2: b,
579 order,
580 });
581 self.adjacency[a.0 as usize].push((b, bidx));
582 self.adjacency[b.0 as usize].push((a, bidx));
583 Ok(bidx)
584 }
585
586 pub fn remove_bond(&mut self, idx: BondIdx) {
589 let removed = idx.0 as usize;
590 if removed >= self.bonds.len() {
591 return;
592 }
593 self.bonds.remove(removed);
594 let n = self.atoms.len();
596 self.adjacency = vec![vec![]; n];
597 for (bidx, bond) in self.bonds.iter().enumerate() {
598 let bi = BondIdx(bidx as u32);
599 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
600 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
601 }
602 }
603
604 pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
606 self.atoms[idx.0 as usize].charge = charge;
607 }
608
609 pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
613 let a = &mut self.atoms[idx.0 as usize];
614 a.element = el;
615 a.chirality = crate::atom::Chirality::None;
616 a.hydrogen_count = None;
617 a.aromatic = false;
618 }
619
620 pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
622 self.atoms[idx.0 as usize].cip_code = code;
623 }
624
625 pub fn stereo_groups(&self) -> &[StereoGroup] {
627 &self.stereo_groups
628 }
629
630 pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
632 self.stereo_groups = groups;
633 }
634
635 pub fn add_stereo_group(&mut self, group: StereoGroup) {
637 self.stereo_groups.push(group);
638 }
639
640 pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
646 self.stereo_neighbor_order
647 .get(&idx.0)
648 .map(|v| v.as_slice())
649 }
650
651 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
653 self.stereo_neighbor_order.insert(idx.0, order);
654 }
655}
656
657impl Molecule {
662 pub fn is_connected(&self) -> bool {
665 let n = self.atoms.len();
666 if n == 0 {
667 return true;
668 }
669 let mut visited = vec![false; n];
670 let mut stack = vec![AtomIdx(0)];
671 visited[0] = true;
672 let mut count = 1;
673 while let Some(cur) = stack.pop() {
674 for (nb, _) in self.neighbors(cur) {
675 if !visited[nb.0 as usize] {
676 visited[nb.0 as usize] = true;
677 count += 1;
678 stack.push(nb);
679 }
680 }
681 }
682 count == n
683 }
684
685 pub fn fragments(&self) -> Vec<Molecule> {
690 let n = self.atoms.len();
691 if n == 0 {
692 return vec![];
693 }
694
695 let mut component: Vec<usize> = vec![usize::MAX; n];
696 let mut comp_id = 0;
697
698 for start in 0..n {
699 if component[start] != usize::MAX {
700 continue;
701 }
702 let mut stack = vec![start];
703 component[start] = comp_id;
704 while let Some(cur) = stack.pop() {
705 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
706 let ni = nb.0 as usize;
707 if component[ni] == usize::MAX {
708 component[ni] = comp_id;
709 stack.push(ni);
710 }
711 }
712 }
713 comp_id += 1;
714 }
715
716 (0..comp_id)
717 .map(|cid| {
718 let mut builder = MoleculeBuilder::new();
719 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
720 std::collections::HashMap::new();
721 for (aidx, atom) in self.atoms() {
722 if component[aidx.0 as usize] == cid {
723 let new_idx = builder.add_atom(atom.clone());
724 old_to_new.insert(aidx, new_idx);
725 }
726 }
727 for (_, bond) in self.bonds() {
728 if let (Some(&a1), Some(&a2)) =
729 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
730 {
731 let _ = builder.add_bond(a1, a2, bond.order);
732 }
733 }
734 builder.build()
735 })
736 .collect()
737 }
738}
739
740#[derive(Default)]
744pub struct MoleculeBuilder {
745 atoms: Vec<Atom>,
746 bonds: Vec<BondEntry>,
747 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
748 stereo_groups: Vec<StereoGroup>,
749 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
750}
751
752impl MoleculeBuilder {
753 pub fn new() -> Self {
754 Self::default()
755 }
756
757 pub fn from_molecule(mol: &Molecule) -> Self {
762 let mut b = Self::new();
763 for (_, atom) in mol.atoms() {
764 b.add_atom(atom.clone());
765 }
766 for (_, bond) in mol.bonds() {
767 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
768 }
769 b.stereo_groups = mol.stereo_groups.clone();
770 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
771 b
772 }
773
774 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
776 self.stereo_neighbor_order.insert(idx.0, order);
777 }
778
779 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
781 self.stereo_neighbor_order.remove(&idx.0);
782 }
783
784 pub fn add_stereo_group(&mut self, group: StereoGroup) {
786 self.stereo_groups.push(group);
787 }
788
789 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
791 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
792 }
793
794 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
802 &self.atoms[idx.0 as usize]
803 }
804
805 pub fn atom_count(&self) -> usize {
807 self.atoms.len()
808 }
809
810 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
813 self.adjacency[idx.0 as usize]
814 .iter()
815 .map(|&(nb, bidx)| (bidx, nb))
816 }
817
818 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
820 let idx = AtomIdx(self.atoms.len() as u32);
821 self.atoms.push(atom);
822 self.adjacency.push(Vec::new());
823 idx
824 }
825
826 pub fn add_bond(
830 &mut self,
831 a: AtomIdx,
832 b: AtomIdx,
833 order: BondOrder,
834 ) -> Result<BondIdx, MolError> {
835 let n = self.atoms.len() as u32;
836 if a.0 >= n {
837 return Err(MolError::InvalidAtomIdx(a));
838 }
839 if b.0 >= n {
840 return Err(MolError::InvalidAtomIdx(b));
841 }
842
843 for &(nb, _) in &self.adjacency[a.0 as usize] {
845 if nb == b {
846 return Err(MolError::DuplicateBond(a, b));
847 }
848 }
849
850 let bidx = BondIdx(self.bonds.len() as u32);
851 self.bonds.push(BondEntry {
852 atom1: a,
853 atom2: b,
854 order,
855 });
856 self.adjacency[a.0 as usize].push((b, bidx));
857 self.adjacency[b.0 as usize].push((a, bidx));
858 Ok(bidx)
859 }
860
861 pub fn build(self) -> Molecule {
863 Molecule {
864 atoms: self.atoms,
865 bonds: self.bonds,
866 adjacency: self.adjacency,
867 stereo_groups: self.stereo_groups,
868 stereo_neighbor_order: self.stereo_neighbor_order,
869 }
870 }
871}
872
873#[cfg(test)]
874mod tests {
875 use super::*;
876 use crate::atom::Atom;
877 use crate::element::Element;
878
879 fn ethane() -> Molecule {
880 let mut b = MoleculeBuilder::new();
881 let c1 = b.add_atom(Atom::new(Element::C));
882 let c2 = b.add_atom(Atom::new(Element::C));
883 b.add_bond(c1, c2, BondOrder::Single).unwrap();
884 b.build()
885 }
886
887 #[test]
888 fn test_basic_molecule() {
889 let mol = ethane();
890 assert_eq!(mol.atom_count(), 2);
891 assert_eq!(mol.bond_count(), 1);
892 }
893
894 #[test]
895 fn test_adjacency() {
896 let mol = ethane();
897 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
898 assert_eq!(neighbors.len(), 1);
899 assert_eq!(neighbors[0].0, AtomIdx(1));
900 }
901
902 #[test]
903 fn test_bond_between() {
904 let mol = ethane();
905 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
906 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
907 }
908
909 #[test]
910 fn test_duplicate_bond_error() {
911 let mut b = MoleculeBuilder::new();
912 let c1 = b.add_atom(Atom::new(Element::C));
913 let c2 = b.add_atom(Atom::new(Element::C));
914 b.add_bond(c1, c2, BondOrder::Single).unwrap();
915 let err = b.add_bond(c1, c2, BondOrder::Double);
916 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
917 }
918
919 #[test]
920 fn test_formula() {
921 let mut b = MoleculeBuilder::new();
922 let c = b.add_atom(Atom::new(Element::C));
923 let n = b.add_atom(Atom::new(Element::N));
924 b.add_bond(c, n, BondOrder::Single).unwrap();
925 let mol = b.build();
926 assert_eq!(mol.formula(), "CN");
927 }
928
929 #[test]
930 fn test_implicit_hydrogen_count() {
931 let mut b = MoleculeBuilder::new();
933 b.add_atom(Atom::organic(Element::C));
934 let mol = b.build();
935 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
936 }
937
938 #[test]
939 fn test_total_formula_methane() {
940 let mut b = MoleculeBuilder::new();
942 b.add_atom(Atom::organic(Element::C));
943 let mol = b.build();
944 assert_eq!(mol.total_formula(), "CH4");
945 }
946
947 #[test]
948 fn test_total_formula_no_hydrogen() {
949 let mut b = MoleculeBuilder::new();
951 let na = b.add_atom(Atom::new(Element::NA));
952 let cl = b.add_atom(Atom::new(Element::CL));
953 b.add_bond(na, cl, BondOrder::Single).unwrap();
954 let mol = b.build();
955 assert_eq!(mol.total_formula(), "ClNa");
956 }
957
958 #[test]
959 fn test_with_atom_aromatic() {
960 let mol = ethane();
961 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
962 assert!(updated.atom(AtomIdx(0)).aromatic);
963 assert!(!updated.atom(AtomIdx(1)).aromatic);
964 }
965
966 #[test]
967 fn test_with_bond_order() {
968 let mol = ethane();
969 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
970 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
971 }
972
973 #[test]
976 fn test_add_remove_atom() {
977 let mut mol = ethane();
978 let n_idx = mol.add_atom(Atom::new(Element::N));
979 assert_eq!(mol.atom_count(), 3);
980 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
981
982 let remap = mol.remove_atom(n_idx);
983 assert_eq!(mol.atom_count(), 2);
984 assert!(remap[n_idx.0 as usize].is_none());
985 }
986
987 #[test]
988 fn test_add_remove_bond() {
989 let mut mol = ethane();
990 let n_idx = mol.add_atom(Atom::new(Element::N));
991 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
992 assert_eq!(mol.bond_count(), 2);
993 mol.remove_bond(bidx);
994 assert_eq!(mol.bond_count(), 1);
995 }
996
997 #[test]
998 fn test_set_charge_element() {
999 let mut mol = ethane();
1000 mol.set_charge(AtomIdx(0), 1);
1001 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1002 mol.set_element(AtomIdx(0), Element::N);
1003 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1004 }
1005
1006 #[test]
1007 fn test_is_connected() {
1008 let mol = ethane();
1009 assert!(mol.is_connected());
1010
1011 let mut b = MoleculeBuilder::new();
1013 b.add_atom(Atom::new(Element::C));
1014 b.add_atom(Atom::new(Element::N));
1015 let disconnected = b.build();
1016 assert!(!disconnected.is_connected());
1017 }
1018
1019 #[test]
1020 fn test_fragments() {
1021 let mut b = MoleculeBuilder::new();
1023 let c1 = b.add_atom(Atom::organic(Element::C));
1024 let c2 = b.add_atom(Atom::organic(Element::C));
1025 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1026 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1028 let frags = mol.fragments();
1029 assert_eq!(frags.len(), 2);
1030 let sizes: std::collections::HashSet<usize> =
1031 frags.iter().map(|f| f.atom_count()).collect();
1032 assert!(sizes.contains(&2));
1033 assert!(sizes.contains(&1));
1034 }
1035
1036 #[test]
1037 fn test_builder_from_molecule() {
1038 let mol = ethane();
1039 let mut b = MoleculeBuilder::from_molecule(&mol);
1040 b.add_atom(Atom::new(Element::O));
1041 let mol2 = b.build();
1042 assert_eq!(mol2.atom_count(), 3);
1043 assert_eq!(mol2.bond_count(), 1); }
1045}