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