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 let mut bond_remap: Vec<Option<BondIdx>> = vec![None; self.bonds.len()];
382 for (old_bidx, bond) in self.bonds() {
383 if bond.atom1 == idx || bond.atom2 == idx {
384 continue;
385 }
386 if let (Some(a1), Some(a2)) =
387 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
388 && let Ok(new_bidx) = builder.add_bond(a1, a2, bond.order)
389 {
390 bond_remap[old_bidx.0 as usize] = Some(new_bidx);
391 }
392 }
393 for (old_key, order) in &self.stereo_neighbor_order {
395 let old_atom = *old_key as usize;
396 if old_atom == removed {
397 continue; }
399 if let Some(Some(new_key)) = remap.get(old_atom) {
400 let new_order: Vec<u32> = order
401 .iter()
402 .filter_map(|&v| {
403 if v == STEREO_H_SENTINEL {
404 Some(STEREO_H_SENTINEL)
405 } else if v as usize == removed {
406 None } else {
408 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
409 }
410 })
411 .collect();
412 builder.set_stereo_neighbor_order(*new_key, new_order);
413 }
414 }
415 for (old_bidx, direction) in &self.bond_directions {
416 if let Some(Some(new_bidx)) = bond_remap.get(*old_bidx as usize) {
417 builder.set_bond_direction(*new_bidx, *direction);
418 }
419 }
420 (builder.build(), remap)
421 }
422
423 pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
427 crate::valence::implicit_hcount(self, idx)
428 }
429
430 pub fn total_formula(&self) -> String {
436 use std::collections::BTreeMap;
437 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
438 let mut implicit_h: u32 = 0;
439 for (aidx, atom) in self.atoms() {
440 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
441 implicit_h += crate::valence::implicit_hcount(self, aidx) as u32;
442 }
443 *counts.entry("H").or_insert(0) += implicit_h;
444 Self::format_hill_order_formula(&counts)
445 }
446
447 pub fn formula_with_isotopes(&self) -> String {
453 use std::collections::BTreeMap;
454 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
456 let mut has_carbon = false;
457 let mut has_explicit_h = false;
458 for (_, atom) in self.atoms() {
459 let sym = atom.element.symbol();
460 let key = match atom.isotope {
461 Some(n) => format!("{n}{sym}"),
462 None => sym.to_string(),
463 };
464 if sym == "C" && atom.isotope.is_none() {
465 has_carbon = true;
466 }
467 if sym == "H" {
468 has_explicit_h = true;
469 }
470 *counts.entry(key).or_insert(0) += 1;
471 }
472
473 let push_count = |key: &str, n: u32, out: &mut String| {
474 out.push_str(key);
475 if n > 1 {
476 out.push_str(&n.to_string());
477 }
478 };
479
480 let mut result = String::new();
481 if has_carbon && let Some(c) = counts.remove("C") {
483 push_count("C", c, &mut result);
484 }
485 if has_explicit_h && let Some(h) = counts.remove("H") {
486 push_count("H", h, &mut result);
487 }
488 for (key, count) in &counts {
489 push_count(key, *count, &mut result);
490 }
491 result
492 }
493
494 pub fn with_atom_aromatic(&self, idx: AtomIdx, aromatic: bool) -> Molecule {
496 let mut builder = MoleculeBuilder::new();
497 for (aidx, atom) in self.atoms() {
498 let mut a = atom.clone();
499 if aidx == idx {
500 a.aromatic = aromatic;
501 }
502 builder.add_atom(a);
503 }
504 for (_, bond) in self.bonds() {
505 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
506 }
507 builder.copy_stereo_from(self);
508 builder.copy_bond_directions_from(self);
509 builder.build()
510 }
511
512 pub fn with_bond_order(&self, idx: BondIdx, order: BondOrder) -> Molecule {
514 let mut builder = MoleculeBuilder::new();
515 for (_, atom) in self.atoms() {
516 builder.add_atom(atom.clone());
517 }
518 for (bidx, bond) in self.bonds() {
519 let o = if bidx == idx { order } else { bond.order };
520 let _ = builder.add_bond(bond.atom1, bond.atom2, o);
521 }
522 builder.copy_stereo_from(self);
523 builder.copy_bond_directions_from(self);
524 builder.build()
525 }
526
527 pub fn with_bond_removed(&self, idx: BondIdx) -> Molecule {
535 let mut builder = MoleculeBuilder::new();
536 for (_, atom) in self.atoms() {
537 builder.add_atom(atom.clone());
538 }
539 for (bidx, bond) in self.bonds() {
540 if bidx == idx {
541 continue;
542 }
543 if let Ok(new_bidx) = builder.add_bond(bond.atom1, bond.atom2, bond.order)
544 && let Some(direction) = self.bond_direction(bidx)
545 {
546 builder.set_bond_direction(new_bidx, direction);
547 }
548 }
549 builder.copy_stereo_from(self);
550 builder.build()
551 }
552}
553
554impl Molecule {
559 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
561 let idx = AtomIdx(self.atoms.len() as u32);
562 self.atoms.push(atom);
563 self.adjacency.push(vec![]);
564 idx
565 }
566
567 pub fn remove_atom(&mut self, idx: AtomIdx) -> Vec<Option<AtomIdx>> {
573 let n = self.atoms.len();
574 let removed = idx.0 as usize;
575
576 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
577 let mut new_pos = 0u32;
578 for (old, slot) in remap.iter_mut().enumerate() {
579 if old == removed {
580 continue;
581 }
582 *slot = Some(AtomIdx(new_pos));
583 new_pos += 1;
584 }
585
586 self.atoms.remove(removed);
587
588 let mut new_bonds: Vec<BondEntry> = Vec::new();
593 let mut bond_remap: Vec<Option<u32>> = vec![None; self.bonds.len()];
594 for (old_bidx, bond) in self.bonds.iter().enumerate() {
595 if bond.atom1 == idx || bond.atom2 == idx {
596 continue;
597 }
598 if let (Some(a1), Some(a2)) =
599 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
600 {
601 bond_remap[old_bidx] = Some(new_bonds.len() as u32);
602 new_bonds.push(BondEntry {
603 atom1: a1,
604 atom2: a2,
605 order: bond.order,
606 });
607 }
608 }
609 self.bonds = new_bonds;
610
611 let old_bond_directions = std::mem::take(&mut self.bond_directions);
613 for (old_key, direction) in old_bond_directions {
614 if let Some(Some(new_key)) = bond_remap.get(old_key as usize) {
615 self.bond_directions.insert(*new_key, direction);
616 }
617 }
618
619 let new_n = self.atoms.len();
621 self.adjacency = vec![vec![]; new_n];
622 for (bidx, bond) in self.bonds.iter().enumerate() {
623 let bi = BondIdx(bidx as u32);
624 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
625 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
626 }
627
628 let old_stereo = std::mem::take(&mut self.stereo_neighbor_order);
630 for (old_key, order) in old_stereo {
631 let old_atom = old_key as usize;
632 if old_atom == removed {
633 continue;
634 }
635 if let Some(Some(new_key)) = remap.get(old_atom) {
636 let new_order: Vec<u32> = order
637 .iter()
638 .filter_map(|&v| {
639 if v == STEREO_H_SENTINEL {
640 Some(STEREO_H_SENTINEL)
641 } else if v as usize == removed {
642 None
643 } else {
644 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
645 }
646 })
647 .collect();
648 self.stereo_neighbor_order.insert(new_key.0, new_order);
649 }
650 }
651
652 remap
653 }
654
655 pub fn add_bond(
659 &mut self,
660 a: AtomIdx,
661 b: AtomIdx,
662 order: BondOrder,
663 ) -> Result<BondIdx, MolError> {
664 let n = self.atoms.len() as u32;
665 if a.0 >= n {
666 return Err(MolError::InvalidAtomIdx(a));
667 }
668 if b.0 >= n {
669 return Err(MolError::InvalidAtomIdx(b));
670 }
671 if self.adjacency[a.0 as usize].iter().any(|&(nb, _)| nb == b) {
672 return Err(MolError::DuplicateBond(a, b));
673 }
674 let bidx = BondIdx(self.bonds.len() as u32);
675 self.bonds.push(BondEntry {
676 atom1: a,
677 atom2: b,
678 order,
679 });
680 self.adjacency[a.0 as usize].push((b, bidx));
681 self.adjacency[b.0 as usize].push((a, bidx));
682 Ok(bidx)
683 }
684
685 pub fn remove_bond(&mut self, idx: BondIdx) {
688 let removed = idx.0 as usize;
689 if removed >= self.bonds.len() {
690 return;
691 }
692 self.bonds.remove(removed);
693 let old_bond_directions = std::mem::take(&mut self.bond_directions);
702 for (old_key, direction) in old_bond_directions {
703 let old = old_key as usize;
704 match old.cmp(&removed) {
705 std::cmp::Ordering::Less => {
706 self.bond_directions.insert(old_key, direction);
707 }
708 std::cmp::Ordering::Equal => {} std::cmp::Ordering::Greater => {
710 self.bond_directions.insert(old_key - 1, direction);
711 }
712 }
713 }
714 let n = self.atoms.len();
716 self.adjacency = vec![vec![]; n];
717 for (bidx, bond) in self.bonds.iter().enumerate() {
718 let bi = BondIdx(bidx as u32);
719 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
720 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
721 }
722 }
723
724 pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
726 self.atoms[idx.0 as usize].charge = charge;
727 }
728
729 pub fn set_isotope(&mut self, idx: AtomIdx, isotope: Option<u16>) {
732 self.atoms[idx.0 as usize].isotope = isotope;
733 }
734
735 pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
739 let a = &mut self.atoms[idx.0 as usize];
740 a.element = el;
741 a.chirality = crate::atom::Chirality::None;
742 a.hydrogen_count = None;
743 a.aromatic = false;
744 }
745
746 pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
748 self.atoms[idx.0 as usize].cip_code = code;
749 }
750
751 pub fn set_chirality(&mut self, idx: AtomIdx, chirality: crate::atom::Chirality) {
753 self.atoms[idx.0 as usize].chirality = chirality;
754 }
755
756 pub fn stereo_groups(&self) -> &[StereoGroup] {
758 &self.stereo_groups
759 }
760
761 pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
763 self.stereo_groups = groups;
764 }
765
766 pub fn add_stereo_group(&mut self, group: StereoGroup) {
768 self.stereo_groups.push(group);
769 }
770
771 pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
777 self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
778 }
779
780 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
782 self.stereo_neighbor_order.insert(idx.0, order);
783 }
784
785 pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
789 self.bond_directions.get(&idx.0).copied()
790 }
791
792 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
794 self.bond_directions.insert(idx.0, direction);
795 }
796}
797
798impl Molecule {
803 pub fn is_connected(&self) -> bool {
806 let n = self.atoms.len();
807 if n == 0 {
808 return true;
809 }
810 let mut visited = vec![false; n];
811 let mut stack = vec![AtomIdx(0)];
812 visited[0] = true;
813 let mut count = 1;
814 while let Some(cur) = stack.pop() {
815 for (nb, _) in self.neighbors(cur) {
816 if !visited[nb.0 as usize] {
817 visited[nb.0 as usize] = true;
818 count += 1;
819 stack.push(nb);
820 }
821 }
822 }
823 count == n
824 }
825
826 pub fn fragments(&self) -> Vec<Molecule> {
831 let n = self.atoms.len();
832 if n == 0 {
833 return vec![];
834 }
835
836 let mut component: Vec<usize> = vec![usize::MAX; n];
837 let mut comp_id = 0;
838
839 for start in 0..n {
840 if component[start] != usize::MAX {
841 continue;
842 }
843 let mut stack = vec![start];
844 component[start] = comp_id;
845 while let Some(cur) = stack.pop() {
846 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
847 let ni = nb.0 as usize;
848 if component[ni] == usize::MAX {
849 component[ni] = comp_id;
850 stack.push(ni);
851 }
852 }
853 }
854 comp_id += 1;
855 }
856
857 (0..comp_id)
858 .map(|cid| {
859 let mut builder = MoleculeBuilder::new();
860 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
861 std::collections::HashMap::new();
862 for (aidx, atom) in self.atoms() {
863 if component[aidx.0 as usize] == cid {
864 let new_idx = builder.add_atom(atom.clone());
865 old_to_new.insert(aidx, new_idx);
866 }
867 }
868 for (_, bond) in self.bonds() {
869 if let (Some(&a1), Some(&a2)) =
870 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
871 {
872 let _ = builder.add_bond(a1, a2, bond.order);
873 }
874 }
875 builder.build()
876 })
877 .collect()
878 }
879}
880
881#[derive(Default)]
885pub struct MoleculeBuilder {
886 atoms: Vec<Atom>,
887 bonds: Vec<BondEntry>,
888 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
889 stereo_groups: Vec<StereoGroup>,
890 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
891 bond_directions: std::collections::HashMap<u32, BondOrder>,
892}
893
894impl MoleculeBuilder {
895 pub fn new() -> Self {
896 Self::default()
897 }
898
899 pub fn from_molecule(mol: &Molecule) -> Self {
904 let mut b = Self::new();
905 for (_, atom) in mol.atoms() {
906 b.add_atom(atom.clone());
907 }
908 for (_, bond) in mol.bonds() {
909 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
910 }
911 b.stereo_groups = mol.stereo_groups.clone();
912 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
913 b.bond_directions = mol.bond_directions.clone();
914 b
915 }
916
917 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
919 self.stereo_neighbor_order.insert(idx.0, order);
920 }
921
922 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
924 self.stereo_neighbor_order.remove(&idx.0);
925 }
926
927 pub fn add_stereo_group(&mut self, group: StereoGroup) {
929 self.stereo_groups.push(group);
930 }
931
932 pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
938 self.stereo_groups = mol.stereo_groups.clone();
939 }
940
941 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
943 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
944 }
945
946 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
948 self.bond_directions.insert(idx.0, direction);
949 }
950
951 pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
959 self.bond_directions = mol.bond_directions.clone();
960 }
961
962 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
970 &self.atoms[idx.0 as usize]
971 }
972
973 pub fn atom_count(&self) -> usize {
975 self.atoms.len()
976 }
977
978 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
981 self.adjacency[idx.0 as usize]
982 .iter()
983 .map(|&(nb, bidx)| (bidx, nb))
984 }
985
986 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
988 let idx = AtomIdx(self.atoms.len() as u32);
989 self.atoms.push(atom);
990 self.adjacency.push(Vec::new());
991 idx
992 }
993
994 pub fn add_bond(
998 &mut self,
999 a: AtomIdx,
1000 b: AtomIdx,
1001 order: BondOrder,
1002 ) -> Result<BondIdx, MolError> {
1003 let n = self.atoms.len() as u32;
1004 if a.0 >= n {
1005 return Err(MolError::InvalidAtomIdx(a));
1006 }
1007 if b.0 >= n {
1008 return Err(MolError::InvalidAtomIdx(b));
1009 }
1010
1011 for &(nb, _) in &self.adjacency[a.0 as usize] {
1013 if nb == b {
1014 return Err(MolError::DuplicateBond(a, b));
1015 }
1016 }
1017
1018 let bidx = BondIdx(self.bonds.len() as u32);
1019 self.bonds.push(BondEntry {
1020 atom1: a,
1021 atom2: b,
1022 order,
1023 });
1024 self.adjacency[a.0 as usize].push((b, bidx));
1025 self.adjacency[b.0 as usize].push((a, bidx));
1026 Ok(bidx)
1027 }
1028
1029 pub fn build(self) -> Molecule {
1031 Molecule {
1032 atoms: self.atoms,
1033 bonds: self.bonds,
1034 adjacency: self.adjacency,
1035 stereo_groups: self.stereo_groups,
1036 stereo_neighbor_order: self.stereo_neighbor_order,
1037 bond_directions: self.bond_directions,
1038 }
1039 }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045 use crate::atom::Atom;
1046 use crate::element::Element;
1047
1048 fn ethane() -> Molecule {
1049 let mut b = MoleculeBuilder::new();
1050 let c1 = b.add_atom(Atom::new(Element::C));
1051 let c2 = b.add_atom(Atom::new(Element::C));
1052 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1053 b.build()
1054 }
1055
1056 #[test]
1057 fn test_basic_molecule() {
1058 let mol = ethane();
1059 assert_eq!(mol.atom_count(), 2);
1060 assert_eq!(mol.bond_count(), 1);
1061 }
1062
1063 #[test]
1064 fn test_adjacency() {
1065 let mol = ethane();
1066 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1067 assert_eq!(neighbors.len(), 1);
1068 assert_eq!(neighbors[0].0, AtomIdx(1));
1069 }
1070
1071 #[test]
1072 fn test_bond_between() {
1073 let mol = ethane();
1074 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1075 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1076 }
1077
1078 #[test]
1079 fn test_duplicate_bond_error() {
1080 let mut b = MoleculeBuilder::new();
1081 let c1 = b.add_atom(Atom::new(Element::C));
1082 let c2 = b.add_atom(Atom::new(Element::C));
1083 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1084 let err = b.add_bond(c1, c2, BondOrder::Double);
1085 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1086 }
1087
1088 #[test]
1089 fn test_formula() {
1090 let mut b = MoleculeBuilder::new();
1091 let c = b.add_atom(Atom::new(Element::C));
1092 let n = b.add_atom(Atom::new(Element::N));
1093 b.add_bond(c, n, BondOrder::Single).unwrap();
1094 let mol = b.build();
1095 assert_eq!(mol.formula(), "CN");
1096 }
1097
1098 #[test]
1099 fn test_implicit_hydrogen_count() {
1100 let mut b = MoleculeBuilder::new();
1102 b.add_atom(Atom::organic(Element::C));
1103 let mol = b.build();
1104 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1105 }
1106
1107 #[test]
1108 fn test_total_formula_methane() {
1109 let mut b = MoleculeBuilder::new();
1111 b.add_atom(Atom::organic(Element::C));
1112 let mol = b.build();
1113 assert_eq!(mol.total_formula(), "CH4");
1114 }
1115
1116 #[test]
1117 fn test_total_formula_no_hydrogen() {
1118 let mut b = MoleculeBuilder::new();
1120 let na = b.add_atom(Atom::new(Element::NA));
1121 let cl = b.add_atom(Atom::new(Element::CL));
1122 b.add_bond(na, cl, BondOrder::Single).unwrap();
1123 let mol = b.build();
1124 assert_eq!(mol.total_formula(), "ClNa");
1125 }
1126
1127 #[test]
1128 fn test_with_atom_aromatic() {
1129 let mol = ethane();
1130 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1131 assert!(updated.atom(AtomIdx(0)).aromatic);
1132 assert!(!updated.atom(AtomIdx(1)).aromatic);
1133 }
1134
1135 #[test]
1136 fn test_with_bond_order() {
1137 let mol = ethane();
1138 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1139 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1140 }
1141
1142 fn chain_with_direction_on_last_bond() -> (Molecule, BondIdx) {
1150 let mut b = MoleculeBuilder::new();
1151 let a = b.add_atom(Atom::new(Element::C));
1152 let bb = b.add_atom(Atom::new(Element::C));
1153 let c = b.add_atom(Atom::new(Element::C));
1154 let d = b.add_atom(Atom::new(Element::C));
1155 b.add_bond(a, bb, BondOrder::Single).unwrap(); b.add_bond(bb, c, BondOrder::Single).unwrap(); let cd = b.add_bond(c, d, BondOrder::Single).unwrap(); b.set_bond_direction(cd, BondOrder::Up);
1159 (b.build(), cd)
1160 }
1161
1162 #[test]
1163 fn test_remove_bond_remaps_bond_direction_not_misattributes() {
1164 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1165 assert_eq!(mol.bond_count(), 3);
1166 mol.remove_bond(BondIdx(0)); assert_eq!(mol.bond_count(), 2);
1168 assert_eq!(mol.bond_direction(BondIdx(1)), Some(BondOrder::Up));
1170 assert_eq!(mol.bond_direction(BondIdx(0)), None);
1174 assert_eq!(mol.bond_opt(BondIdx(2)), None);
1175 }
1176
1177 #[test]
1178 fn test_remove_bond_drops_direction_for_the_removed_bond_itself() {
1179 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1180 mol.remove_bond(BondIdx(2)); assert_eq!(mol.bond_count(), 2);
1182 assert!(mol.bond_direction(BondIdx(0)).is_none());
1183 assert!(mol.bond_direction(BondIdx(1)).is_none());
1184 }
1185
1186 #[test]
1187 fn test_with_atom_removed_remaps_bond_direction() {
1188 let (mol, _cd) = chain_with_direction_on_last_bond();
1189 let (updated, _atom_remap) = mol.with_atom_removed(AtomIdx(0));
1193 assert_eq!(updated.bond_count(), 2);
1194 let has_direction = (0..updated.bond_count())
1198 .map(|i| BondIdx(i as u32))
1199 .any(|bidx| updated.bond_direction(bidx) == Some(BondOrder::Up));
1200 assert!(
1201 has_direction,
1202 "bond_direction on C-D must survive atom removal, remapped to its new bond index"
1203 );
1204 }
1205
1206 #[test]
1209 fn test_add_remove_atom() {
1210 let mut mol = ethane();
1211 let n_idx = mol.add_atom(Atom::new(Element::N));
1212 assert_eq!(mol.atom_count(), 3);
1213 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1214
1215 let remap = mol.remove_atom(n_idx);
1216 assert_eq!(mol.atom_count(), 2);
1217 assert!(remap[n_idx.0 as usize].is_none());
1218 }
1219
1220 #[test]
1221 fn test_add_remove_bond() {
1222 let mut mol = ethane();
1223 let n_idx = mol.add_atom(Atom::new(Element::N));
1224 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1225 assert_eq!(mol.bond_count(), 2);
1226 mol.remove_bond(bidx);
1227 assert_eq!(mol.bond_count(), 1);
1228 }
1229
1230 #[test]
1231 fn test_set_charge_element() {
1232 let mut mol = ethane();
1233 mol.set_charge(AtomIdx(0), 1);
1234 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1235 mol.set_element(AtomIdx(0), Element::N);
1236 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1237 }
1238
1239 #[test]
1240 fn test_is_connected() {
1241 let mol = ethane();
1242 assert!(mol.is_connected());
1243
1244 let mut b = MoleculeBuilder::new();
1246 b.add_atom(Atom::new(Element::C));
1247 b.add_atom(Atom::new(Element::N));
1248 let disconnected = b.build();
1249 assert!(!disconnected.is_connected());
1250 }
1251
1252 #[test]
1253 fn test_fragments() {
1254 let mut b = MoleculeBuilder::new();
1256 let c1 = b.add_atom(Atom::organic(Element::C));
1257 let c2 = b.add_atom(Atom::organic(Element::C));
1258 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1259 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1261 let frags = mol.fragments();
1262 assert_eq!(frags.len(), 2);
1263 let sizes: std::collections::HashSet<usize> =
1264 frags.iter().map(|f| f.atom_count()).collect();
1265 assert!(sizes.contains(&2));
1266 assert!(sizes.contains(&1));
1267 }
1268
1269 #[test]
1270 fn test_builder_from_molecule() {
1271 let mol = ethane();
1272 let mut b = MoleculeBuilder::from_molecule(&mol);
1273 b.add_atom(Atom::new(Element::O));
1274 let mol2 = b.build();
1275 assert_eq!(mol2.atom_count(), 3);
1276 assert_eq!(mol2.bond_count(), 1); }
1278
1279 #[test]
1282 fn test_atom_opt_valid() {
1283 let mol = ethane();
1284 assert!(mol.atom_opt(AtomIdx(0)).is_some());
1285 assert!(mol.atom_opt(AtomIdx(1)).is_some());
1286 let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1287 assert_eq!(atom.element.atomic_number(), 6);
1288 }
1289
1290 #[test]
1291 fn test_atom_opt_invalid() {
1292 let mol = ethane();
1293 assert!(mol.atom_opt(AtomIdx(2)).is_none());
1294 assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1295 }
1296
1297 #[test]
1298 fn test_bond_opt_valid() {
1299 let mol = ethane();
1300 assert!(mol.bond_opt(BondIdx(0)).is_some());
1301 let bond = mol.bond_opt(BondIdx(0)).unwrap();
1302 assert_eq!(bond.order, BondOrder::Single);
1303 }
1304
1305 #[test]
1306 fn test_bond_opt_invalid() {
1307 let mol = ethane();
1308 assert!(mol.bond_opt(BondIdx(1)).is_none());
1309 assert!(mol.bond_opt(BondIdx(1000)).is_none());
1310 }
1311
1312 #[test]
1313 fn test_neighbors_opt_valid() {
1314 let mol = ethane();
1315 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1316 assert_eq!(neighbors.len(), 1);
1317 assert_eq!(neighbors[0].0, AtomIdx(1));
1318 }
1319
1320 #[test]
1321 fn test_neighbors_opt_isolated_atom() {
1322 let mut b = MoleculeBuilder::new();
1323 b.add_atom(Atom::new(Element::C));
1324 b.add_atom(Atom::new(Element::N));
1325 let mol = b.build();
1326 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1327 assert_eq!(neighbors.len(), 0);
1328 }
1329
1330 #[test]
1331 fn test_neighbors_opt_invalid() {
1332 let mol = ethane();
1333 assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1334 assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1335 }
1336
1337 #[test]
1338 fn test_degree_opt_valid() {
1339 let mol = ethane();
1340 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1341 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1342 }
1343
1344 #[test]
1345 fn test_degree_opt_isolated_atom() {
1346 let mut b = MoleculeBuilder::new();
1347 b.add_atom(Atom::new(Element::C));
1348 b.add_atom(Atom::new(Element::N));
1349 let mol = b.build();
1350 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1351 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1352 }
1353
1354 #[test]
1355 fn test_degree_opt_invalid() {
1356 let mol = ethane();
1357 assert!(mol.degree_opt(AtomIdx(2)).is_none());
1358 assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1359 }
1360
1361 #[test]
1362 fn test_degree_opt_multiple_bonds() {
1363 let mut b = MoleculeBuilder::new();
1365 let center = b.add_atom(Atom::new(Element::C));
1366 let n1 = b.add_atom(Atom::new(Element::C));
1367 let n2 = b.add_atom(Atom::new(Element::N));
1368 let n3 = b.add_atom(Atom::new(Element::O));
1369 b.add_bond(center, n1, BondOrder::Single).unwrap();
1370 b.add_bond(center, n2, BondOrder::Double).unwrap();
1371 b.add_bond(center, n3, BondOrder::Single).unwrap();
1372 let mol = b.build();
1373 assert_eq!(mol.degree_opt(center), Some(3));
1374 assert_eq!(mol.degree_opt(n1), Some(1));
1375 assert_eq!(mol.degree_opt(n2), Some(1));
1376 assert_eq!(mol.degree_opt(n3), Some(1));
1377 }
1378}