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
45#[derive(Clone)]
46pub struct Molecule {
47 atoms: Vec<Atom>,
48 bonds: Vec<BondEntry>,
49 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
51 stereo_groups: Vec<StereoGroup>,
53 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
61 bond_directions: std::collections::HashMap<u32, BondOrder>,
68}
69
70impl Molecule {
71 pub fn atom_count(&self) -> usize {
73 self.atoms.len()
74 }
75
76 pub fn bond_count(&self) -> usize {
78 self.bonds.len()
79 }
80
81 pub fn atom(&self, idx: AtomIdx) -> &Atom {
88 let i = idx.0 as usize;
89 if i >= self.atoms.len() {
90 panic!(
91 "atom index {} out of range (molecule has {} atoms)",
92 idx.0,
93 self.atoms.len()
94 );
95 }
96 &self.atoms[i]
97 }
98
99 pub fn atom_opt(&self, idx: AtomIdx) -> Option<&Atom> {
101 let i = idx.0 as usize;
102 if i < self.atoms.len() {
103 Some(&self.atoms[i])
104 } else {
105 None
106 }
107 }
108
109 pub fn bond(&self, idx: BondIdx) -> &BondEntry {
116 let i = idx.0 as usize;
117 if i >= self.bonds.len() {
118 panic!(
119 "bond index {} out of range (molecule has {} bonds)",
120 idx.0,
121 self.bonds.len()
122 );
123 }
124 &self.bonds[i]
125 }
126
127 pub fn bond_opt(&self, idx: BondIdx) -> Option<&BondEntry> {
129 let i = idx.0 as usize;
130 if i < self.bonds.len() {
131 Some(&self.bonds[i])
132 } else {
133 None
134 }
135 }
136
137 pub fn atoms(&self) -> impl Iterator<Item = (AtomIdx, &Atom)> {
139 self.atoms
140 .iter()
141 .enumerate()
142 .map(|(i, a)| (AtomIdx(i as u32), a))
143 }
144
145 pub fn bonds(&self) -> impl Iterator<Item = (BondIdx, &BondEntry)> {
147 self.bonds
148 .iter()
149 .enumerate()
150 .map(|(i, b)| (BondIdx(i as u32), b))
151 }
152
153 pub fn neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (AtomIdx, BondIdx)> + '_ {
160 let i = idx.0 as usize;
161 if i >= self.adjacency.len() {
162 panic!(
163 "atom index {} out of range (molecule has {} atoms)",
164 idx.0,
165 self.adjacency.len()
166 );
167 }
168 self.adjacency[i].iter().copied()
169 }
170
171 pub fn neighbors_opt(&self, idx: AtomIdx) -> Option<Vec<(AtomIdx, BondIdx)>> {
173 let i = idx.0 as usize;
174 if i < self.adjacency.len() {
175 Some(self.adjacency[i].to_vec())
176 } else {
177 None
178 }
179 }
180
181 pub fn degree(&self, idx: AtomIdx) -> usize {
188 let i = idx.0 as usize;
189 if i >= self.adjacency.len() {
190 panic!(
191 "atom index {} out of range (molecule has {} atoms)",
192 idx.0,
193 self.adjacency.len()
194 );
195 }
196 self.adjacency[i].len()
197 }
198
199 pub fn degree_opt(&self, idx: AtomIdx) -> Option<usize> {
201 let i = idx.0 as usize;
202 if i < self.adjacency.len() {
203 Some(self.adjacency[i].len())
204 } else {
205 None
206 }
207 }
208
209 pub fn bond_between(&self, a: AtomIdx, b: AtomIdx) -> Option<(BondIdx, &BondEntry)> {
211 let a_idx = a.0 as usize;
212 let b_idx = b.0 as usize;
213 if a_idx >= self.adjacency.len() || b_idx >= self.atoms.len() {
214 return None;
215 }
216 self.adjacency[a_idx]
217 .iter()
218 .find(|&&(nb, _)| nb == b)
219 .and_then(|&(_, bidx)| {
220 let bond_idx = bidx.0 as usize;
221 if bond_idx < self.bonds.len() {
222 Some((bidx, &self.bonds[bond_idx]))
223 } else {
224 None
225 }
226 })
227 }
228
229 pub fn formula(&self) -> String {
231 use std::collections::BTreeMap;
232 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
233 for (_, atom) in self.atoms() {
234 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
235 }
236 let mut result = Self::format_hill_order_formula(&counts);
237 let total_charge: i32 = self.atoms().map(|(_, a)| a.charge as i32).sum();
238 match total_charge {
239 0 => {}
240 1 => result.push('+'),
241 -1 => result.push('-'),
242 n if n > 0 => result.push_str(&format!("+{n}")),
243 n => result.push_str(&n.to_string()),
244 }
245 result
246 }
247}
248
249impl Molecule {
254 fn format_hill_order_formula(counts: &std::collections::BTreeMap<&str, u32>) -> String {
256 let mut counts = counts.clone();
257 let mut result = String::new();
258 let push_count = |sym: &str, n: u32, out: &mut String| {
259 out.push_str(sym);
260 if n > 1 {
261 out.push_str(&n.to_string());
262 }
263 };
264 if let Some(c) = counts.remove("C") {
265 push_count("C", c, &mut result);
266 }
267 if let Some(h) = counts.remove("H")
268 && h > 0
269 {
270 push_count("H", h, &mut result);
271 }
272 for (sym, count) in &counts {
273 push_count(sym, *count, &mut result);
274 }
275 result
276 }
277
278 pub fn with_atom_added(&self, atom: Atom) -> (Molecule, AtomIdx) {
281 let mut builder = MoleculeBuilder::from_molecule(self);
282 let new_idx = builder.add_atom(atom);
283 (builder.build(), new_idx)
284 }
285
286 pub fn with_bond_added(
292 &self,
293 a: AtomIdx,
294 b: AtomIdx,
295 order: BondOrder,
296 ) -> Result<(Molecule, BondIdx), MolError> {
297 let mut builder = MoleculeBuilder::from_molecule(self);
298 let bond_idx = builder.add_bond(a, b, order)?;
299 Ok((builder.build(), bond_idx))
300 }
301
302 pub fn with_atom_charge(&self, idx: AtomIdx, charge: i8) -> Molecule {
304 let mut builder = MoleculeBuilder::new();
305 for (aidx, atom) in self.atoms() {
306 let mut a = atom.clone();
307 if aidx == idx {
308 a.charge = charge;
309 }
310 builder.add_atom(a);
311 }
312 for (_, bond) in self.bonds() {
313 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
314 }
315 builder.copy_stereo_from(self);
316 builder.copy_bond_directions_from(self);
317 builder.build()
318 }
319
320 pub fn with_atom_element(&self, idx: AtomIdx, el: Element) -> Molecule {
325 let mut builder = MoleculeBuilder::new();
326 for (aidx, atom) in self.atoms() {
327 let mut a = atom.clone();
328 if aidx == idx {
329 a.element = el;
330 a.chirality = crate::atom::Chirality::None;
332 a.hydrogen_count = None;
333 a.aromatic = false;
334 }
335 builder.add_atom(a);
336 }
337 for (_, bond) in self.bonds() {
338 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
339 }
340 builder.copy_stereo_from(self);
341 builder.copy_bond_directions_from(self);
342 builder.clear_stereo_neighbor_order(idx);
344 builder.build()
345 }
346
347 pub fn with_atom_removed(&self, idx: AtomIdx) -> (Molecule, Vec<Option<AtomIdx>>) {
354 let n = self.atom_count();
355 let removed = idx.0 as usize;
356
357 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
359 let mut new_pos = 0u32;
360 for (old, slot) in remap.iter_mut().enumerate() {
361 if old == removed {
362 continue;
363 }
364 *slot = Some(AtomIdx(new_pos));
365 new_pos += 1;
366 }
367
368 let mut builder = MoleculeBuilder::new();
369 for (aidx, atom) in self.atoms() {
370 if aidx == idx {
371 continue;
372 }
373 builder.add_atom(atom.clone());
374 }
375 for (_, bond) in self.bonds() {
376 if bond.atom1 == idx || bond.atom2 == idx {
377 continue;
378 }
379 if let (Some(a1), Some(a2)) =
380 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
381 {
382 let _ = builder.add_bond(a1, a2, bond.order);
383 }
384 }
385 for (old_key, order) in &self.stereo_neighbor_order {
387 let old_atom = *old_key as usize;
388 if old_atom == removed {
389 continue; }
391 if let Some(Some(new_key)) = remap.get(old_atom) {
392 let new_order: Vec<u32> = order
393 .iter()
394 .filter_map(|&v| {
395 if v == STEREO_H_SENTINEL {
396 Some(STEREO_H_SENTINEL)
397 } else if v as usize == removed {
398 None } else {
400 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
401 }
402 })
403 .collect();
404 builder.set_stereo_neighbor_order(*new_key, new_order);
405 }
406 }
407 (builder.build(), remap)
408 }
409
410 pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
414 crate::valence::implicit_hcount(self, idx)
415 }
416
417 pub fn total_formula(&self) -> String {
423 use std::collections::BTreeMap;
424 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
425 let mut implicit_h: u32 = 0;
426 for (aidx, atom) in self.atoms() {
427 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
428 implicit_h += crate::valence::implicit_hcount(self, aidx) as u32;
429 }
430 *counts.entry("H").or_insert(0) += implicit_h;
431 Self::format_hill_order_formula(&counts)
432 }
433
434 pub fn formula_with_isotopes(&self) -> String {
440 use std::collections::BTreeMap;
441 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
443 let mut has_carbon = false;
444 let mut has_explicit_h = false;
445 for (_, atom) in self.atoms() {
446 let sym = atom.element.symbol();
447 let key = match atom.isotope {
448 Some(n) => format!("{n}{sym}"),
449 None => sym.to_string(),
450 };
451 if sym == "C" && atom.isotope.is_none() {
452 has_carbon = true;
453 }
454 if sym == "H" {
455 has_explicit_h = true;
456 }
457 *counts.entry(key).or_insert(0) += 1;
458 }
459
460 let push_count = |key: &str, n: u32, out: &mut String| {
461 out.push_str(key);
462 if n > 1 {
463 out.push_str(&n.to_string());
464 }
465 };
466
467 let mut result = String::new();
468 if has_carbon && let Some(c) = counts.remove("C") {
470 push_count("C", c, &mut result);
471 }
472 if has_explicit_h && let Some(h) = counts.remove("H") {
473 push_count("H", h, &mut result);
474 }
475 for (key, count) in &counts {
476 push_count(key, *count, &mut result);
477 }
478 result
479 }
480
481 pub fn with_atom_aromatic(&self, idx: AtomIdx, aromatic: bool) -> Molecule {
483 let mut builder = MoleculeBuilder::new();
484 for (aidx, atom) in self.atoms() {
485 let mut a = atom.clone();
486 if aidx == idx {
487 a.aromatic = aromatic;
488 }
489 builder.add_atom(a);
490 }
491 for (_, bond) in self.bonds() {
492 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
493 }
494 builder.copy_stereo_from(self);
495 builder.copy_bond_directions_from(self);
496 builder.build()
497 }
498
499 pub fn with_bond_order(&self, idx: BondIdx, order: BondOrder) -> Molecule {
501 let mut builder = MoleculeBuilder::new();
502 for (_, atom) in self.atoms() {
503 builder.add_atom(atom.clone());
504 }
505 for (bidx, bond) in self.bonds() {
506 let o = if bidx == idx { order } else { bond.order };
507 let _ = builder.add_bond(bond.atom1, bond.atom2, o);
508 }
509 builder.copy_stereo_from(self);
510 builder.copy_bond_directions_from(self);
511 builder.build()
512 }
513
514 pub fn with_bond_removed(&self, idx: BondIdx) -> Molecule {
522 let mut builder = MoleculeBuilder::new();
523 for (_, atom) in self.atoms() {
524 builder.add_atom(atom.clone());
525 }
526 for (bidx, bond) in self.bonds() {
527 if bidx == idx {
528 continue;
529 }
530 if let Ok(new_bidx) = builder.add_bond(bond.atom1, bond.atom2, bond.order)
531 && let Some(direction) = self.bond_direction(bidx)
532 {
533 builder.set_bond_direction(new_bidx, direction);
534 }
535 }
536 builder.copy_stereo_from(self);
537 builder.build()
538 }
539}
540
541impl Molecule {
546 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
548 let idx = AtomIdx(self.atoms.len() as u32);
549 self.atoms.push(atom);
550 self.adjacency.push(vec![]);
551 idx
552 }
553
554 pub fn remove_atom(&mut self, idx: AtomIdx) -> Vec<Option<AtomIdx>> {
560 let n = self.atoms.len();
561 let removed = idx.0 as usize;
562
563 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
564 let mut new_pos = 0u32;
565 for (old, slot) in remap.iter_mut().enumerate() {
566 if old == removed {
567 continue;
568 }
569 *slot = Some(AtomIdx(new_pos));
570 new_pos += 1;
571 }
572
573 self.atoms.remove(removed);
574
575 let mut new_bonds: Vec<BondEntry> = Vec::new();
580 let mut bond_remap: Vec<Option<u32>> = vec![None; self.bonds.len()];
581 for (old_bidx, bond) in self.bonds.iter().enumerate() {
582 if bond.atom1 == idx || bond.atom2 == idx {
583 continue;
584 }
585 if let (Some(a1), Some(a2)) =
586 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
587 {
588 bond_remap[old_bidx] = Some(new_bonds.len() as u32);
589 new_bonds.push(BondEntry {
590 atom1: a1,
591 atom2: a2,
592 order: bond.order,
593 });
594 }
595 }
596 self.bonds = new_bonds;
597
598 let old_bond_directions = std::mem::take(&mut self.bond_directions);
600 for (old_key, direction) in old_bond_directions {
601 if let Some(Some(new_key)) = bond_remap.get(old_key as usize) {
602 self.bond_directions.insert(*new_key, direction);
603 }
604 }
605
606 let new_n = self.atoms.len();
608 self.adjacency = vec![vec![]; new_n];
609 for (bidx, bond) in self.bonds.iter().enumerate() {
610 let bi = BondIdx(bidx as u32);
611 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
612 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
613 }
614
615 let old_stereo = std::mem::take(&mut self.stereo_neighbor_order);
617 for (old_key, order) in old_stereo {
618 let old_atom = old_key as usize;
619 if old_atom == removed {
620 continue;
621 }
622 if let Some(Some(new_key)) = remap.get(old_atom) {
623 let new_order: Vec<u32> = order
624 .iter()
625 .filter_map(|&v| {
626 if v == STEREO_H_SENTINEL {
627 Some(STEREO_H_SENTINEL)
628 } else if v as usize == removed {
629 None
630 } else {
631 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
632 }
633 })
634 .collect();
635 self.stereo_neighbor_order.insert(new_key.0, new_order);
636 }
637 }
638
639 remap
640 }
641
642 pub fn add_bond(
646 &mut self,
647 a: AtomIdx,
648 b: AtomIdx,
649 order: BondOrder,
650 ) -> Result<BondIdx, MolError> {
651 let n = self.atoms.len() as u32;
652 if a.0 >= n {
653 return Err(MolError::InvalidAtomIdx(a));
654 }
655 if b.0 >= n {
656 return Err(MolError::InvalidAtomIdx(b));
657 }
658 if self.adjacency[a.0 as usize].iter().any(|&(nb, _)| nb == b) {
659 return Err(MolError::DuplicateBond(a, b));
660 }
661 let bidx = BondIdx(self.bonds.len() as u32);
662 self.bonds.push(BondEntry {
663 atom1: a,
664 atom2: b,
665 order,
666 });
667 self.adjacency[a.0 as usize].push((b, bidx));
668 self.adjacency[b.0 as usize].push((a, bidx));
669 Ok(bidx)
670 }
671
672 pub fn remove_bond(&mut self, idx: BondIdx) {
675 let removed = idx.0 as usize;
676 if removed >= self.bonds.len() {
677 return;
678 }
679 self.bonds.remove(removed);
680 let n = self.atoms.len();
682 self.adjacency = vec![vec![]; n];
683 for (bidx, bond) in self.bonds.iter().enumerate() {
684 let bi = BondIdx(bidx as u32);
685 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
686 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
687 }
688 }
689
690 pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
692 self.atoms[idx.0 as usize].charge = charge;
693 }
694
695 pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
699 let a = &mut self.atoms[idx.0 as usize];
700 a.element = el;
701 a.chirality = crate::atom::Chirality::None;
702 a.hydrogen_count = None;
703 a.aromatic = false;
704 }
705
706 pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
708 self.atoms[idx.0 as usize].cip_code = code;
709 }
710
711 pub fn set_chirality(&mut self, idx: AtomIdx, chirality: crate::atom::Chirality) {
713 self.atoms[idx.0 as usize].chirality = chirality;
714 }
715
716 pub fn stereo_groups(&self) -> &[StereoGroup] {
718 &self.stereo_groups
719 }
720
721 pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
723 self.stereo_groups = groups;
724 }
725
726 pub fn add_stereo_group(&mut self, group: StereoGroup) {
728 self.stereo_groups.push(group);
729 }
730
731 pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
737 self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
738 }
739
740 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
742 self.stereo_neighbor_order.insert(idx.0, order);
743 }
744
745 pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
749 self.bond_directions.get(&idx.0).copied()
750 }
751
752 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
754 self.bond_directions.insert(idx.0, direction);
755 }
756}
757
758impl Molecule {
763 pub fn is_connected(&self) -> bool {
766 let n = self.atoms.len();
767 if n == 0 {
768 return true;
769 }
770 let mut visited = vec![false; n];
771 let mut stack = vec![AtomIdx(0)];
772 visited[0] = true;
773 let mut count = 1;
774 while let Some(cur) = stack.pop() {
775 for (nb, _) in self.neighbors(cur) {
776 if !visited[nb.0 as usize] {
777 visited[nb.0 as usize] = true;
778 count += 1;
779 stack.push(nb);
780 }
781 }
782 }
783 count == n
784 }
785
786 pub fn fragments(&self) -> Vec<Molecule> {
791 let n = self.atoms.len();
792 if n == 0 {
793 return vec![];
794 }
795
796 let mut component: Vec<usize> = vec![usize::MAX; n];
797 let mut comp_id = 0;
798
799 for start in 0..n {
800 if component[start] != usize::MAX {
801 continue;
802 }
803 let mut stack = vec![start];
804 component[start] = comp_id;
805 while let Some(cur) = stack.pop() {
806 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
807 let ni = nb.0 as usize;
808 if component[ni] == usize::MAX {
809 component[ni] = comp_id;
810 stack.push(ni);
811 }
812 }
813 }
814 comp_id += 1;
815 }
816
817 (0..comp_id)
818 .map(|cid| {
819 let mut builder = MoleculeBuilder::new();
820 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
821 std::collections::HashMap::new();
822 for (aidx, atom) in self.atoms() {
823 if component[aidx.0 as usize] == cid {
824 let new_idx = builder.add_atom(atom.clone());
825 old_to_new.insert(aidx, new_idx);
826 }
827 }
828 for (_, bond) in self.bonds() {
829 if let (Some(&a1), Some(&a2)) =
830 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
831 {
832 let _ = builder.add_bond(a1, a2, bond.order);
833 }
834 }
835 builder.build()
836 })
837 .collect()
838 }
839}
840
841#[derive(Default)]
845pub struct MoleculeBuilder {
846 atoms: Vec<Atom>,
847 bonds: Vec<BondEntry>,
848 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
849 stereo_groups: Vec<StereoGroup>,
850 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
851 bond_directions: std::collections::HashMap<u32, BondOrder>,
852}
853
854impl MoleculeBuilder {
855 pub fn new() -> Self {
856 Self::default()
857 }
858
859 pub fn from_molecule(mol: &Molecule) -> Self {
864 let mut b = Self::new();
865 for (_, atom) in mol.atoms() {
866 b.add_atom(atom.clone());
867 }
868 for (_, bond) in mol.bonds() {
869 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
870 }
871 b.stereo_groups = mol.stereo_groups.clone();
872 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
873 b.bond_directions = mol.bond_directions.clone();
874 b
875 }
876
877 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
879 self.stereo_neighbor_order.insert(idx.0, order);
880 }
881
882 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
884 self.stereo_neighbor_order.remove(&idx.0);
885 }
886
887 pub fn add_stereo_group(&mut self, group: StereoGroup) {
889 self.stereo_groups.push(group);
890 }
891
892 pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
898 self.stereo_groups = mol.stereo_groups.clone();
899 }
900
901 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
903 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
904 }
905
906 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
908 self.bond_directions.insert(idx.0, direction);
909 }
910
911 pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
919 self.bond_directions = mol.bond_directions.clone();
920 }
921
922 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
930 &self.atoms[idx.0 as usize]
931 }
932
933 pub fn atom_count(&self) -> usize {
935 self.atoms.len()
936 }
937
938 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
941 self.adjacency[idx.0 as usize]
942 .iter()
943 .map(|&(nb, bidx)| (bidx, nb))
944 }
945
946 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
948 let idx = AtomIdx(self.atoms.len() as u32);
949 self.atoms.push(atom);
950 self.adjacency.push(Vec::new());
951 idx
952 }
953
954 pub fn add_bond(
958 &mut self,
959 a: AtomIdx,
960 b: AtomIdx,
961 order: BondOrder,
962 ) -> Result<BondIdx, MolError> {
963 let n = self.atoms.len() as u32;
964 if a.0 >= n {
965 return Err(MolError::InvalidAtomIdx(a));
966 }
967 if b.0 >= n {
968 return Err(MolError::InvalidAtomIdx(b));
969 }
970
971 for &(nb, _) in &self.adjacency[a.0 as usize] {
973 if nb == b {
974 return Err(MolError::DuplicateBond(a, b));
975 }
976 }
977
978 let bidx = BondIdx(self.bonds.len() as u32);
979 self.bonds.push(BondEntry {
980 atom1: a,
981 atom2: b,
982 order,
983 });
984 self.adjacency[a.0 as usize].push((b, bidx));
985 self.adjacency[b.0 as usize].push((a, bidx));
986 Ok(bidx)
987 }
988
989 pub fn build(self) -> Molecule {
991 Molecule {
992 atoms: self.atoms,
993 bonds: self.bonds,
994 adjacency: self.adjacency,
995 stereo_groups: self.stereo_groups,
996 stereo_neighbor_order: self.stereo_neighbor_order,
997 bond_directions: self.bond_directions,
998 }
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use crate::atom::Atom;
1006 use crate::element::Element;
1007
1008 fn ethane() -> Molecule {
1009 let mut b = MoleculeBuilder::new();
1010 let c1 = b.add_atom(Atom::new(Element::C));
1011 let c2 = b.add_atom(Atom::new(Element::C));
1012 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1013 b.build()
1014 }
1015
1016 #[test]
1017 fn test_basic_molecule() {
1018 let mol = ethane();
1019 assert_eq!(mol.atom_count(), 2);
1020 assert_eq!(mol.bond_count(), 1);
1021 }
1022
1023 #[test]
1024 fn test_adjacency() {
1025 let mol = ethane();
1026 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1027 assert_eq!(neighbors.len(), 1);
1028 assert_eq!(neighbors[0].0, AtomIdx(1));
1029 }
1030
1031 #[test]
1032 fn test_bond_between() {
1033 let mol = ethane();
1034 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1035 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1036 }
1037
1038 #[test]
1039 fn test_duplicate_bond_error() {
1040 let mut b = MoleculeBuilder::new();
1041 let c1 = b.add_atom(Atom::new(Element::C));
1042 let c2 = b.add_atom(Atom::new(Element::C));
1043 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1044 let err = b.add_bond(c1, c2, BondOrder::Double);
1045 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1046 }
1047
1048 #[test]
1049 fn test_formula() {
1050 let mut b = MoleculeBuilder::new();
1051 let c = b.add_atom(Atom::new(Element::C));
1052 let n = b.add_atom(Atom::new(Element::N));
1053 b.add_bond(c, n, BondOrder::Single).unwrap();
1054 let mol = b.build();
1055 assert_eq!(mol.formula(), "CN");
1056 }
1057
1058 #[test]
1059 fn test_implicit_hydrogen_count() {
1060 let mut b = MoleculeBuilder::new();
1062 b.add_atom(Atom::organic(Element::C));
1063 let mol = b.build();
1064 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1065 }
1066
1067 #[test]
1068 fn test_total_formula_methane() {
1069 let mut b = MoleculeBuilder::new();
1071 b.add_atom(Atom::organic(Element::C));
1072 let mol = b.build();
1073 assert_eq!(mol.total_formula(), "CH4");
1074 }
1075
1076 #[test]
1077 fn test_total_formula_no_hydrogen() {
1078 let mut b = MoleculeBuilder::new();
1080 let na = b.add_atom(Atom::new(Element::NA));
1081 let cl = b.add_atom(Atom::new(Element::CL));
1082 b.add_bond(na, cl, BondOrder::Single).unwrap();
1083 let mol = b.build();
1084 assert_eq!(mol.total_formula(), "ClNa");
1085 }
1086
1087 #[test]
1088 fn test_with_atom_aromatic() {
1089 let mol = ethane();
1090 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1091 assert!(updated.atom(AtomIdx(0)).aromatic);
1092 assert!(!updated.atom(AtomIdx(1)).aromatic);
1093 }
1094
1095 #[test]
1096 fn test_with_bond_order() {
1097 let mol = ethane();
1098 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1099 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1100 }
1101
1102 #[test]
1105 fn test_add_remove_atom() {
1106 let mut mol = ethane();
1107 let n_idx = mol.add_atom(Atom::new(Element::N));
1108 assert_eq!(mol.atom_count(), 3);
1109 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1110
1111 let remap = mol.remove_atom(n_idx);
1112 assert_eq!(mol.atom_count(), 2);
1113 assert!(remap[n_idx.0 as usize].is_none());
1114 }
1115
1116 #[test]
1117 fn test_add_remove_bond() {
1118 let mut mol = ethane();
1119 let n_idx = mol.add_atom(Atom::new(Element::N));
1120 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1121 assert_eq!(mol.bond_count(), 2);
1122 mol.remove_bond(bidx);
1123 assert_eq!(mol.bond_count(), 1);
1124 }
1125
1126 #[test]
1127 fn test_set_charge_element() {
1128 let mut mol = ethane();
1129 mol.set_charge(AtomIdx(0), 1);
1130 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1131 mol.set_element(AtomIdx(0), Element::N);
1132 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1133 }
1134
1135 #[test]
1136 fn test_is_connected() {
1137 let mol = ethane();
1138 assert!(mol.is_connected());
1139
1140 let mut b = MoleculeBuilder::new();
1142 b.add_atom(Atom::new(Element::C));
1143 b.add_atom(Atom::new(Element::N));
1144 let disconnected = b.build();
1145 assert!(!disconnected.is_connected());
1146 }
1147
1148 #[test]
1149 fn test_fragments() {
1150 let mut b = MoleculeBuilder::new();
1152 let c1 = b.add_atom(Atom::organic(Element::C));
1153 let c2 = b.add_atom(Atom::organic(Element::C));
1154 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1155 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1157 let frags = mol.fragments();
1158 assert_eq!(frags.len(), 2);
1159 let sizes: std::collections::HashSet<usize> =
1160 frags.iter().map(|f| f.atom_count()).collect();
1161 assert!(sizes.contains(&2));
1162 assert!(sizes.contains(&1));
1163 }
1164
1165 #[test]
1166 fn test_builder_from_molecule() {
1167 let mol = ethane();
1168 let mut b = MoleculeBuilder::from_molecule(&mol);
1169 b.add_atom(Atom::new(Element::O));
1170 let mol2 = b.build();
1171 assert_eq!(mol2.atom_count(), 3);
1172 assert_eq!(mol2.bond_count(), 1); }
1174
1175 #[test]
1178 fn test_atom_opt_valid() {
1179 let mol = ethane();
1180 assert!(mol.atom_opt(AtomIdx(0)).is_some());
1181 assert!(mol.atom_opt(AtomIdx(1)).is_some());
1182 let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1183 assert_eq!(atom.element.atomic_number(), 6);
1184 }
1185
1186 #[test]
1187 fn test_atom_opt_invalid() {
1188 let mol = ethane();
1189 assert!(mol.atom_opt(AtomIdx(2)).is_none());
1190 assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1191 }
1192
1193 #[test]
1194 fn test_bond_opt_valid() {
1195 let mol = ethane();
1196 assert!(mol.bond_opt(BondIdx(0)).is_some());
1197 let bond = mol.bond_opt(BondIdx(0)).unwrap();
1198 assert_eq!(bond.order, BondOrder::Single);
1199 }
1200
1201 #[test]
1202 fn test_bond_opt_invalid() {
1203 let mol = ethane();
1204 assert!(mol.bond_opt(BondIdx(1)).is_none());
1205 assert!(mol.bond_opt(BondIdx(1000)).is_none());
1206 }
1207
1208 #[test]
1209 fn test_neighbors_opt_valid() {
1210 let mol = ethane();
1211 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1212 assert_eq!(neighbors.len(), 1);
1213 assert_eq!(neighbors[0].0, AtomIdx(1));
1214 }
1215
1216 #[test]
1217 fn test_neighbors_opt_isolated_atom() {
1218 let mut b = MoleculeBuilder::new();
1219 b.add_atom(Atom::new(Element::C));
1220 b.add_atom(Atom::new(Element::N));
1221 let mol = b.build();
1222 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1223 assert_eq!(neighbors.len(), 0);
1224 }
1225
1226 #[test]
1227 fn test_neighbors_opt_invalid() {
1228 let mol = ethane();
1229 assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1230 assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1231 }
1232
1233 #[test]
1234 fn test_degree_opt_valid() {
1235 let mol = ethane();
1236 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1237 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1238 }
1239
1240 #[test]
1241 fn test_degree_opt_isolated_atom() {
1242 let mut b = MoleculeBuilder::new();
1243 b.add_atom(Atom::new(Element::C));
1244 b.add_atom(Atom::new(Element::N));
1245 let mol = b.build();
1246 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1247 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1248 }
1249
1250 #[test]
1251 fn test_degree_opt_invalid() {
1252 let mol = ethane();
1253 assert!(mol.degree_opt(AtomIdx(2)).is_none());
1254 assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1255 }
1256
1257 #[test]
1258 fn test_degree_opt_multiple_bonds() {
1259 let mut b = MoleculeBuilder::new();
1261 let center = b.add_atom(Atom::new(Element::C));
1262 let n1 = b.add_atom(Atom::new(Element::C));
1263 let n2 = b.add_atom(Atom::new(Element::N));
1264 let n3 = b.add_atom(Atom::new(Element::O));
1265 b.add_bond(center, n1, BondOrder::Single).unwrap();
1266 b.add_bond(center, n2, BondOrder::Double).unwrap();
1267 b.add_bond(center, n3, BondOrder::Single).unwrap();
1268 let mol = b.build();
1269 assert_eq!(mol.degree_opt(center), Some(3));
1270 assert_eq!(mol.degree_opt(n1), Some(1));
1271 assert_eq!(mol.degree_opt(n2), Some(1));
1272 assert_eq!(mol.degree_opt(n3), Some(1));
1273 }
1274}