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 set_bond_order(&mut self, idx: BondIdx, order: BondOrder) {
764 self.bonds[idx.0 as usize].order = order;
765 }
766
767 pub fn stereo_groups(&self) -> &[StereoGroup] {
769 &self.stereo_groups
770 }
771
772 pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
774 self.stereo_groups = groups;
775 }
776
777 pub fn add_stereo_group(&mut self, group: StereoGroup) {
779 self.stereo_groups.push(group);
780 }
781
782 pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
788 self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
789 }
790
791 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
793 self.stereo_neighbor_order.insert(idx.0, order);
794 }
795
796 pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
800 self.bond_directions.get(&idx.0).copied()
801 }
802
803 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
805 self.bond_directions.insert(idx.0, direction);
806 }
807}
808
809impl Molecule {
814 pub fn is_connected(&self) -> bool {
817 let n = self.atoms.len();
818 if n == 0 {
819 return true;
820 }
821 let mut visited = vec![false; n];
822 let mut stack = vec![AtomIdx(0)];
823 visited[0] = true;
824 let mut count = 1;
825 while let Some(cur) = stack.pop() {
826 for (nb, _) in self.neighbors(cur) {
827 if !visited[nb.0 as usize] {
828 visited[nb.0 as usize] = true;
829 count += 1;
830 stack.push(nb);
831 }
832 }
833 }
834 count == n
835 }
836
837 pub fn fragments(&self) -> Vec<Molecule> {
842 let n = self.atoms.len();
843 if n == 0 {
844 return vec![];
845 }
846
847 let mut component: Vec<usize> = vec![usize::MAX; n];
848 let mut comp_id = 0;
849
850 for start in 0..n {
851 if component[start] != usize::MAX {
852 continue;
853 }
854 let mut stack = vec![start];
855 component[start] = comp_id;
856 while let Some(cur) = stack.pop() {
857 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
858 let ni = nb.0 as usize;
859 if component[ni] == usize::MAX {
860 component[ni] = comp_id;
861 stack.push(ni);
862 }
863 }
864 }
865 comp_id += 1;
866 }
867
868 (0..comp_id)
869 .map(|cid| {
870 let mut builder = MoleculeBuilder::new();
871 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
872 std::collections::HashMap::new();
873 for (aidx, atom) in self.atoms() {
874 if component[aidx.0 as usize] == cid {
875 let new_idx = builder.add_atom(atom.clone());
876 old_to_new.insert(aidx, new_idx);
877 }
878 }
879 for (_, bond) in self.bonds() {
880 if let (Some(&a1), Some(&a2)) =
881 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
882 {
883 let _ = builder.add_bond(a1, a2, bond.order);
884 }
885 }
886 builder.build()
887 })
888 .collect()
889 }
890}
891
892#[derive(Default)]
896pub struct MoleculeBuilder {
897 atoms: Vec<Atom>,
898 bonds: Vec<BondEntry>,
899 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
900 stereo_groups: Vec<StereoGroup>,
901 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
902 bond_directions: std::collections::HashMap<u32, BondOrder>,
903}
904
905impl MoleculeBuilder {
906 pub fn new() -> Self {
907 Self::default()
908 }
909
910 pub fn from_molecule(mol: &Molecule) -> Self {
915 let mut b = Self::new();
916 for (_, atom) in mol.atoms() {
917 b.add_atom(atom.clone());
918 }
919 for (_, bond) in mol.bonds() {
920 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
921 }
922 b.stereo_groups = mol.stereo_groups.clone();
923 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
924 b.bond_directions = mol.bond_directions.clone();
925 b
926 }
927
928 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
930 self.stereo_neighbor_order.insert(idx.0, order);
931 }
932
933 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
935 self.stereo_neighbor_order.remove(&idx.0);
936 }
937
938 pub fn add_stereo_group(&mut self, group: StereoGroup) {
940 self.stereo_groups.push(group);
941 }
942
943 pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
949 self.stereo_groups = mol.stereo_groups.clone();
950 }
951
952 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
954 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
955 }
956
957 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
959 self.bond_directions.insert(idx.0, direction);
960 }
961
962 pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
970 self.bond_directions = mol.bond_directions.clone();
971 }
972
973 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
981 &self.atoms[idx.0 as usize]
982 }
983
984 pub fn atom_count(&self) -> usize {
986 self.atoms.len()
987 }
988
989 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
992 self.adjacency[idx.0 as usize]
993 .iter()
994 .map(|&(nb, bidx)| (bidx, nb))
995 }
996
997 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
999 let idx = AtomIdx(self.atoms.len() as u32);
1000 self.atoms.push(atom);
1001 self.adjacency.push(Vec::new());
1002 idx
1003 }
1004
1005 pub fn add_bond(
1009 &mut self,
1010 a: AtomIdx,
1011 b: AtomIdx,
1012 order: BondOrder,
1013 ) -> Result<BondIdx, MolError> {
1014 let n = self.atoms.len() as u32;
1015 if a.0 >= n {
1016 return Err(MolError::InvalidAtomIdx(a));
1017 }
1018 if b.0 >= n {
1019 return Err(MolError::InvalidAtomIdx(b));
1020 }
1021
1022 for &(nb, _) in &self.adjacency[a.0 as usize] {
1024 if nb == b {
1025 return Err(MolError::DuplicateBond(a, b));
1026 }
1027 }
1028
1029 let bidx = BondIdx(self.bonds.len() as u32);
1030 self.bonds.push(BondEntry {
1031 atom1: a,
1032 atom2: b,
1033 order,
1034 });
1035 self.adjacency[a.0 as usize].push((b, bidx));
1036 self.adjacency[b.0 as usize].push((a, bidx));
1037 Ok(bidx)
1038 }
1039
1040 pub fn build(self) -> Molecule {
1042 Molecule {
1043 atoms: self.atoms,
1044 bonds: self.bonds,
1045 adjacency: self.adjacency,
1046 stereo_groups: self.stereo_groups,
1047 stereo_neighbor_order: self.stereo_neighbor_order,
1048 bond_directions: self.bond_directions,
1049 }
1050 }
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056 use crate::atom::Atom;
1057 use crate::element::Element;
1058
1059 fn ethane() -> Molecule {
1060 let mut b = MoleculeBuilder::new();
1061 let c1 = b.add_atom(Atom::new(Element::C));
1062 let c2 = b.add_atom(Atom::new(Element::C));
1063 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1064 b.build()
1065 }
1066
1067 #[test]
1068 fn test_basic_molecule() {
1069 let mol = ethane();
1070 assert_eq!(mol.atom_count(), 2);
1071 assert_eq!(mol.bond_count(), 1);
1072 }
1073
1074 #[test]
1075 fn test_adjacency() {
1076 let mol = ethane();
1077 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1078 assert_eq!(neighbors.len(), 1);
1079 assert_eq!(neighbors[0].0, AtomIdx(1));
1080 }
1081
1082 #[test]
1083 fn test_bond_between() {
1084 let mol = ethane();
1085 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1086 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1087 }
1088
1089 #[test]
1090 fn test_duplicate_bond_error() {
1091 let mut b = MoleculeBuilder::new();
1092 let c1 = b.add_atom(Atom::new(Element::C));
1093 let c2 = b.add_atom(Atom::new(Element::C));
1094 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1095 let err = b.add_bond(c1, c2, BondOrder::Double);
1096 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1097 }
1098
1099 #[test]
1100 fn test_formula() {
1101 let mut b = MoleculeBuilder::new();
1102 let c = b.add_atom(Atom::new(Element::C));
1103 let n = b.add_atom(Atom::new(Element::N));
1104 b.add_bond(c, n, BondOrder::Single).unwrap();
1105 let mol = b.build();
1106 assert_eq!(mol.formula(), "CN");
1107 }
1108
1109 #[test]
1110 fn test_implicit_hydrogen_count() {
1111 let mut b = MoleculeBuilder::new();
1113 b.add_atom(Atom::organic(Element::C));
1114 let mol = b.build();
1115 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1116 }
1117
1118 #[test]
1119 fn test_total_formula_methane() {
1120 let mut b = MoleculeBuilder::new();
1122 b.add_atom(Atom::organic(Element::C));
1123 let mol = b.build();
1124 assert_eq!(mol.total_formula(), "CH4");
1125 }
1126
1127 #[test]
1128 fn test_total_formula_no_hydrogen() {
1129 let mut b = MoleculeBuilder::new();
1131 let na = b.add_atom(Atom::new(Element::NA));
1132 let cl = b.add_atom(Atom::new(Element::CL));
1133 b.add_bond(na, cl, BondOrder::Single).unwrap();
1134 let mol = b.build();
1135 assert_eq!(mol.total_formula(), "ClNa");
1136 }
1137
1138 #[test]
1139 fn test_with_atom_aromatic() {
1140 let mol = ethane();
1141 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1142 assert!(updated.atom(AtomIdx(0)).aromatic);
1143 assert!(!updated.atom(AtomIdx(1)).aromatic);
1144 }
1145
1146 #[test]
1147 fn test_with_bond_order() {
1148 let mol = ethane();
1149 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1150 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1151 }
1152
1153 fn chain_with_direction_on_last_bond() -> (Molecule, BondIdx) {
1161 let mut b = MoleculeBuilder::new();
1162 let a = b.add_atom(Atom::new(Element::C));
1163 let bb = b.add_atom(Atom::new(Element::C));
1164 let c = b.add_atom(Atom::new(Element::C));
1165 let d = b.add_atom(Atom::new(Element::C));
1166 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);
1170 (b.build(), cd)
1171 }
1172
1173 #[test]
1174 fn test_remove_bond_remaps_bond_direction_not_misattributes() {
1175 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1176 assert_eq!(mol.bond_count(), 3);
1177 mol.remove_bond(BondIdx(0)); assert_eq!(mol.bond_count(), 2);
1179 assert_eq!(mol.bond_direction(BondIdx(1)), Some(BondOrder::Up));
1181 assert_eq!(mol.bond_direction(BondIdx(0)), None);
1185 assert_eq!(mol.bond_opt(BondIdx(2)), None);
1186 }
1187
1188 #[test]
1189 fn test_remove_bond_drops_direction_for_the_removed_bond_itself() {
1190 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1191 mol.remove_bond(BondIdx(2)); assert_eq!(mol.bond_count(), 2);
1193 assert!(mol.bond_direction(BondIdx(0)).is_none());
1194 assert!(mol.bond_direction(BondIdx(1)).is_none());
1195 }
1196
1197 #[test]
1198 fn test_with_atom_removed_remaps_bond_direction() {
1199 let (mol, _cd) = chain_with_direction_on_last_bond();
1200 let (updated, _atom_remap) = mol.with_atom_removed(AtomIdx(0));
1204 assert_eq!(updated.bond_count(), 2);
1205 let has_direction = (0..updated.bond_count())
1209 .map(|i| BondIdx(i as u32))
1210 .any(|bidx| updated.bond_direction(bidx) == Some(BondOrder::Up));
1211 assert!(
1212 has_direction,
1213 "bond_direction on C-D must survive atom removal, remapped to its new bond index"
1214 );
1215 }
1216
1217 #[test]
1220 fn test_add_remove_atom() {
1221 let mut mol = ethane();
1222 let n_idx = mol.add_atom(Atom::new(Element::N));
1223 assert_eq!(mol.atom_count(), 3);
1224 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1225
1226 let remap = mol.remove_atom(n_idx);
1227 assert_eq!(mol.atom_count(), 2);
1228 assert!(remap[n_idx.0 as usize].is_none());
1229 }
1230
1231 #[test]
1232 fn test_add_remove_bond() {
1233 let mut mol = ethane();
1234 let n_idx = mol.add_atom(Atom::new(Element::N));
1235 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1236 assert_eq!(mol.bond_count(), 2);
1237 mol.remove_bond(bidx);
1238 assert_eq!(mol.bond_count(), 1);
1239 }
1240
1241 #[test]
1242 fn test_set_charge_element() {
1243 let mut mol = ethane();
1244 mol.set_charge(AtomIdx(0), 1);
1245 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1246 mol.set_element(AtomIdx(0), Element::N);
1247 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1248 }
1249
1250 #[test]
1251 fn test_is_connected() {
1252 let mol = ethane();
1253 assert!(mol.is_connected());
1254
1255 let mut b = MoleculeBuilder::new();
1257 b.add_atom(Atom::new(Element::C));
1258 b.add_atom(Atom::new(Element::N));
1259 let disconnected = b.build();
1260 assert!(!disconnected.is_connected());
1261 }
1262
1263 #[test]
1264 fn test_fragments() {
1265 let mut b = MoleculeBuilder::new();
1267 let c1 = b.add_atom(Atom::organic(Element::C));
1268 let c2 = b.add_atom(Atom::organic(Element::C));
1269 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1270 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1272 let frags = mol.fragments();
1273 assert_eq!(frags.len(), 2);
1274 let sizes: std::collections::HashSet<usize> =
1275 frags.iter().map(|f| f.atom_count()).collect();
1276 assert!(sizes.contains(&2));
1277 assert!(sizes.contains(&1));
1278 }
1279
1280 #[test]
1281 fn test_builder_from_molecule() {
1282 let mol = ethane();
1283 let mut b = MoleculeBuilder::from_molecule(&mol);
1284 b.add_atom(Atom::new(Element::O));
1285 let mol2 = b.build();
1286 assert_eq!(mol2.atom_count(), 3);
1287 assert_eq!(mol2.bond_count(), 1); }
1289
1290 #[test]
1293 fn test_atom_opt_valid() {
1294 let mol = ethane();
1295 assert!(mol.atom_opt(AtomIdx(0)).is_some());
1296 assert!(mol.atom_opt(AtomIdx(1)).is_some());
1297 let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1298 assert_eq!(atom.element.atomic_number(), 6);
1299 }
1300
1301 #[test]
1302 fn test_atom_opt_invalid() {
1303 let mol = ethane();
1304 assert!(mol.atom_opt(AtomIdx(2)).is_none());
1305 assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1306 }
1307
1308 #[test]
1309 fn test_bond_opt_valid() {
1310 let mol = ethane();
1311 assert!(mol.bond_opt(BondIdx(0)).is_some());
1312 let bond = mol.bond_opt(BondIdx(0)).unwrap();
1313 assert_eq!(bond.order, BondOrder::Single);
1314 }
1315
1316 #[test]
1317 fn test_bond_opt_invalid() {
1318 let mol = ethane();
1319 assert!(mol.bond_opt(BondIdx(1)).is_none());
1320 assert!(mol.bond_opt(BondIdx(1000)).is_none());
1321 }
1322
1323 #[test]
1324 fn test_neighbors_opt_valid() {
1325 let mol = ethane();
1326 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1327 assert_eq!(neighbors.len(), 1);
1328 assert_eq!(neighbors[0].0, AtomIdx(1));
1329 }
1330
1331 #[test]
1332 fn test_neighbors_opt_isolated_atom() {
1333 let mut b = MoleculeBuilder::new();
1334 b.add_atom(Atom::new(Element::C));
1335 b.add_atom(Atom::new(Element::N));
1336 let mol = b.build();
1337 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1338 assert_eq!(neighbors.len(), 0);
1339 }
1340
1341 #[test]
1342 fn test_neighbors_opt_invalid() {
1343 let mol = ethane();
1344 assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1345 assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1346 }
1347
1348 #[test]
1349 fn test_degree_opt_valid() {
1350 let mol = ethane();
1351 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1352 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1353 }
1354
1355 #[test]
1356 fn test_degree_opt_isolated_atom() {
1357 let mut b = MoleculeBuilder::new();
1358 b.add_atom(Atom::new(Element::C));
1359 b.add_atom(Atom::new(Element::N));
1360 let mol = b.build();
1361 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1362 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1363 }
1364
1365 #[test]
1366 fn test_degree_opt_invalid() {
1367 let mol = ethane();
1368 assert!(mol.degree_opt(AtomIdx(2)).is_none());
1369 assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1370 }
1371
1372 #[test]
1373 fn test_degree_opt_multiple_bonds() {
1374 let mut b = MoleculeBuilder::new();
1376 let center = b.add_atom(Atom::new(Element::C));
1377 let n1 = b.add_atom(Atom::new(Element::C));
1378 let n2 = b.add_atom(Atom::new(Element::N));
1379 let n3 = b.add_atom(Atom::new(Element::O));
1380 b.add_bond(center, n1, BondOrder::Single).unwrap();
1381 b.add_bond(center, n2, BondOrder::Double).unwrap();
1382 b.add_bond(center, n3, BondOrder::Single).unwrap();
1383 let mol = b.build();
1384 assert_eq!(mol.degree_opt(center), Some(3));
1385 assert_eq!(mol.degree_opt(n1), Some(1));
1386 assert_eq!(mol.degree_opt(n2), Some(1));
1387 assert_eq!(mol.degree_opt(n3), Some(1));
1388 }
1389}