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]> {
806 self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
807 }
808
809 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
811 self.stereo_neighbor_order.insert(idx.0, order);
812 }
813
814 pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
818 self.bond_directions.get(&idx.0).copied()
819 }
820
821 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
823 self.bond_directions.insert(idx.0, direction);
824 }
825}
826
827impl Molecule {
832 pub fn is_connected(&self) -> bool {
835 let n = self.atoms.len();
836 if n == 0 {
837 return true;
838 }
839 let mut visited = vec![false; n];
840 let mut stack = vec![AtomIdx(0)];
841 visited[0] = true;
842 let mut count = 1;
843 while let Some(cur) = stack.pop() {
844 for (nb, _) in self.neighbors(cur) {
845 if !visited[nb.0 as usize] {
846 visited[nb.0 as usize] = true;
847 count += 1;
848 stack.push(nb);
849 }
850 }
851 }
852 count == n
853 }
854
855 pub fn fragments(&self) -> Vec<Molecule> {
860 let n = self.atoms.len();
861 if n == 0 {
862 return vec![];
863 }
864
865 let mut component: Vec<usize> = vec![usize::MAX; n];
866 let mut comp_id = 0;
867
868 for start in 0..n {
869 if component[start] != usize::MAX {
870 continue;
871 }
872 let mut stack = vec![start];
873 component[start] = comp_id;
874 while let Some(cur) = stack.pop() {
875 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
876 let ni = nb.0 as usize;
877 if component[ni] == usize::MAX {
878 component[ni] = comp_id;
879 stack.push(ni);
880 }
881 }
882 }
883 comp_id += 1;
884 }
885
886 (0..comp_id)
887 .map(|cid| {
888 let mut builder = MoleculeBuilder::new();
889 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
890 std::collections::HashMap::new();
891 for (aidx, atom) in self.atoms() {
892 if component[aidx.0 as usize] == cid {
893 let new_idx = builder.add_atom(atom.clone());
894 old_to_new.insert(aidx, new_idx);
895 }
896 }
897 for (_, bond) in self.bonds() {
898 if let (Some(&a1), Some(&a2)) =
899 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
900 {
901 let _ = builder.add_bond(a1, a2, bond.order);
902 }
903 }
904 builder.build()
905 })
906 .collect()
907 }
908}
909
910#[derive(Default)]
914pub struct MoleculeBuilder {
915 atoms: Vec<Atom>,
916 bonds: Vec<BondEntry>,
917 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
918 stereo_groups: Vec<StereoGroup>,
919 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
920 bond_directions: std::collections::HashMap<u32, BondOrder>,
921}
922
923impl MoleculeBuilder {
924 pub fn new() -> Self {
925 Self::default()
926 }
927
928 pub fn from_molecule(mol: &Molecule) -> Self {
933 let mut b = Self::new();
934 for (_, atom) in mol.atoms() {
935 b.add_atom(atom.clone());
936 }
937 for (_, bond) in mol.bonds() {
938 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
939 }
940 b.stereo_groups = mol.stereo_groups.clone();
941 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
942 b.bond_directions = mol.bond_directions.clone();
943 b
944 }
945
946 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
948 self.stereo_neighbor_order.insert(idx.0, order);
949 }
950
951 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
953 self.stereo_neighbor_order.remove(&idx.0);
954 }
955
956 pub fn add_stereo_group(&mut self, group: StereoGroup) {
958 self.stereo_groups.push(group);
959 }
960
961 pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
967 self.stereo_groups = mol.stereo_groups.clone();
968 }
969
970 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
972 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
973 }
974
975 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
977 self.bond_directions.insert(idx.0, direction);
978 }
979
980 pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
988 self.bond_directions = mol.bond_directions.clone();
989 }
990
991 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
999 &self.atoms[idx.0 as usize]
1000 }
1001
1002 pub fn atom_count(&self) -> usize {
1004 self.atoms.len()
1005 }
1006
1007 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
1010 self.adjacency[idx.0 as usize]
1011 .iter()
1012 .map(|&(nb, bidx)| (bidx, nb))
1013 }
1014
1015 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
1017 let idx = AtomIdx(self.atoms.len() as u32);
1018 self.atoms.push(atom);
1019 self.adjacency.push(Vec::new());
1020 idx
1021 }
1022
1023 pub fn add_bond(
1027 &mut self,
1028 a: AtomIdx,
1029 b: AtomIdx,
1030 order: BondOrder,
1031 ) -> Result<BondIdx, MolError> {
1032 let n = self.atoms.len() as u32;
1033 if a.0 >= n {
1034 return Err(MolError::InvalidAtomIdx(a));
1035 }
1036 if b.0 >= n {
1037 return Err(MolError::InvalidAtomIdx(b));
1038 }
1039
1040 for &(nb, _) in &self.adjacency[a.0 as usize] {
1042 if nb == b {
1043 return Err(MolError::DuplicateBond(a, b));
1044 }
1045 }
1046
1047 let bidx = BondIdx(self.bonds.len() as u32);
1048 self.bonds.push(BondEntry {
1049 atom1: a,
1050 atom2: b,
1051 order,
1052 });
1053 self.adjacency[a.0 as usize].push((b, bidx));
1054 self.adjacency[b.0 as usize].push((a, bidx));
1055 Ok(bidx)
1056 }
1057
1058 pub fn build(self) -> Molecule {
1060 Molecule {
1061 atoms: self.atoms,
1062 bonds: self.bonds,
1063 adjacency: self.adjacency,
1064 stereo_groups: self.stereo_groups,
1065 stereo_neighbor_order: self.stereo_neighbor_order,
1066 bond_directions: self.bond_directions,
1067 }
1068 }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074 use crate::atom::Atom;
1075 use crate::element::Element;
1076
1077 fn ethane() -> Molecule {
1078 let mut b = MoleculeBuilder::new();
1079 let c1 = b.add_atom(Atom::new(Element::C));
1080 let c2 = b.add_atom(Atom::new(Element::C));
1081 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1082 b.build()
1083 }
1084
1085 #[test]
1086 fn test_basic_molecule() {
1087 let mol = ethane();
1088 assert_eq!(mol.atom_count(), 2);
1089 assert_eq!(mol.bond_count(), 1);
1090 }
1091
1092 #[test]
1093 fn test_adjacency() {
1094 let mol = ethane();
1095 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1096 assert_eq!(neighbors.len(), 1);
1097 assert_eq!(neighbors[0].0, AtomIdx(1));
1098 }
1099
1100 #[test]
1101 fn test_bond_between() {
1102 let mol = ethane();
1103 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1104 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1105 }
1106
1107 #[test]
1108 fn test_duplicate_bond_error() {
1109 let mut b = MoleculeBuilder::new();
1110 let c1 = b.add_atom(Atom::new(Element::C));
1111 let c2 = b.add_atom(Atom::new(Element::C));
1112 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1113 let err = b.add_bond(c1, c2, BondOrder::Double);
1114 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1115 }
1116
1117 #[test]
1118 fn test_formula() {
1119 let mut b = MoleculeBuilder::new();
1120 let c = b.add_atom(Atom::new(Element::C));
1121 let n = b.add_atom(Atom::new(Element::N));
1122 b.add_bond(c, n, BondOrder::Single).unwrap();
1123 let mol = b.build();
1124 assert_eq!(mol.formula(), "CN");
1125 }
1126
1127 #[test]
1128 fn test_implicit_hydrogen_count() {
1129 let mut b = MoleculeBuilder::new();
1131 b.add_atom(Atom::organic(Element::C));
1132 let mol = b.build();
1133 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1134 }
1135
1136 #[test]
1137 fn test_total_formula_methane() {
1138 let mut b = MoleculeBuilder::new();
1140 b.add_atom(Atom::organic(Element::C));
1141 let mol = b.build();
1142 assert_eq!(mol.total_formula(), "CH4");
1143 }
1144
1145 #[test]
1146 fn test_total_formula_no_hydrogen() {
1147 let mut b = MoleculeBuilder::new();
1149 let na = b.add_atom(Atom::new(Element::NA));
1150 let cl = b.add_atom(Atom::new(Element::CL));
1151 b.add_bond(na, cl, BondOrder::Single).unwrap();
1152 let mol = b.build();
1153 assert_eq!(mol.total_formula(), "ClNa");
1154 }
1155
1156 #[test]
1157 fn test_with_atom_aromatic() {
1158 let mol = ethane();
1159 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1160 assert!(updated.atom(AtomIdx(0)).aromatic);
1161 assert!(!updated.atom(AtomIdx(1)).aromatic);
1162 }
1163
1164 #[test]
1165 fn test_with_bond_order() {
1166 let mol = ethane();
1167 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1168 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1169 }
1170
1171 fn chain_with_direction_on_last_bond() -> (Molecule, BondIdx) {
1179 let mut b = MoleculeBuilder::new();
1180 let a = b.add_atom(Atom::new(Element::C));
1181 let bb = b.add_atom(Atom::new(Element::C));
1182 let c = b.add_atom(Atom::new(Element::C));
1183 let d = b.add_atom(Atom::new(Element::C));
1184 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);
1188 (b.build(), cd)
1189 }
1190
1191 #[test]
1192 fn test_remove_bond_remaps_bond_direction_not_misattributes() {
1193 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1194 assert_eq!(mol.bond_count(), 3);
1195 mol.remove_bond(BondIdx(0)); assert_eq!(mol.bond_count(), 2);
1197 assert_eq!(mol.bond_direction(BondIdx(1)), Some(BondOrder::Up));
1199 assert_eq!(mol.bond_direction(BondIdx(0)), None);
1203 assert_eq!(mol.bond_opt(BondIdx(2)), None);
1204 }
1205
1206 #[test]
1207 fn test_remove_bond_drops_direction_for_the_removed_bond_itself() {
1208 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1209 mol.remove_bond(BondIdx(2)); assert_eq!(mol.bond_count(), 2);
1211 assert!(mol.bond_direction(BondIdx(0)).is_none());
1212 assert!(mol.bond_direction(BondIdx(1)).is_none());
1213 }
1214
1215 #[test]
1216 fn test_with_atom_removed_remaps_bond_direction() {
1217 let (mol, _cd) = chain_with_direction_on_last_bond();
1218 let (updated, _atom_remap) = mol.with_atom_removed(AtomIdx(0));
1222 assert_eq!(updated.bond_count(), 2);
1223 let has_direction = (0..updated.bond_count())
1227 .map(|i| BondIdx(i as u32))
1228 .any(|bidx| updated.bond_direction(bidx) == Some(BondOrder::Up));
1229 assert!(
1230 has_direction,
1231 "bond_direction on C-D must survive atom removal, remapped to its new bond index"
1232 );
1233 }
1234
1235 #[test]
1238 fn test_add_remove_atom() {
1239 let mut mol = ethane();
1240 let n_idx = mol.add_atom(Atom::new(Element::N));
1241 assert_eq!(mol.atom_count(), 3);
1242 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1243
1244 let remap = mol.remove_atom(n_idx);
1245 assert_eq!(mol.atom_count(), 2);
1246 assert!(remap[n_idx.0 as usize].is_none());
1247 }
1248
1249 #[test]
1250 fn test_add_remove_bond() {
1251 let mut mol = ethane();
1252 let n_idx = mol.add_atom(Atom::new(Element::N));
1253 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1254 assert_eq!(mol.bond_count(), 2);
1255 mol.remove_bond(bidx);
1256 assert_eq!(mol.bond_count(), 1);
1257 }
1258
1259 #[test]
1260 fn test_set_charge_element() {
1261 let mut mol = ethane();
1262 mol.set_charge(AtomIdx(0), 1);
1263 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1264 mol.set_element(AtomIdx(0), Element::N);
1265 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1266 }
1267
1268 #[test]
1269 fn test_is_connected() {
1270 let mol = ethane();
1271 assert!(mol.is_connected());
1272
1273 let mut b = MoleculeBuilder::new();
1275 b.add_atom(Atom::new(Element::C));
1276 b.add_atom(Atom::new(Element::N));
1277 let disconnected = b.build();
1278 assert!(!disconnected.is_connected());
1279 }
1280
1281 #[test]
1282 fn test_fragments() {
1283 let mut b = MoleculeBuilder::new();
1285 let c1 = b.add_atom(Atom::organic(Element::C));
1286 let c2 = b.add_atom(Atom::organic(Element::C));
1287 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1288 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1290 let frags = mol.fragments();
1291 assert_eq!(frags.len(), 2);
1292 let sizes: std::collections::HashSet<usize> =
1293 frags.iter().map(|f| f.atom_count()).collect();
1294 assert!(sizes.contains(&2));
1295 assert!(sizes.contains(&1));
1296 }
1297
1298 #[test]
1299 fn test_builder_from_molecule() {
1300 let mol = ethane();
1301 let mut b = MoleculeBuilder::from_molecule(&mol);
1302 b.add_atom(Atom::new(Element::O));
1303 let mol2 = b.build();
1304 assert_eq!(mol2.atom_count(), 3);
1305 assert_eq!(mol2.bond_count(), 1); }
1307
1308 #[test]
1311 fn test_atom_opt_valid() {
1312 let mol = ethane();
1313 assert!(mol.atom_opt(AtomIdx(0)).is_some());
1314 assert!(mol.atom_opt(AtomIdx(1)).is_some());
1315 let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1316 assert_eq!(atom.element.atomic_number(), 6);
1317 }
1318
1319 #[test]
1320 fn test_atom_opt_invalid() {
1321 let mol = ethane();
1322 assert!(mol.atom_opt(AtomIdx(2)).is_none());
1323 assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1324 }
1325
1326 #[test]
1327 fn test_bond_opt_valid() {
1328 let mol = ethane();
1329 assert!(mol.bond_opt(BondIdx(0)).is_some());
1330 let bond = mol.bond_opt(BondIdx(0)).unwrap();
1331 assert_eq!(bond.order, BondOrder::Single);
1332 }
1333
1334 #[test]
1335 fn test_bond_opt_invalid() {
1336 let mol = ethane();
1337 assert!(mol.bond_opt(BondIdx(1)).is_none());
1338 assert!(mol.bond_opt(BondIdx(1000)).is_none());
1339 }
1340
1341 #[test]
1342 fn test_neighbors_opt_valid() {
1343 let mol = ethane();
1344 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1345 assert_eq!(neighbors.len(), 1);
1346 assert_eq!(neighbors[0].0, AtomIdx(1));
1347 }
1348
1349 #[test]
1350 fn test_neighbors_opt_isolated_atom() {
1351 let mut b = MoleculeBuilder::new();
1352 b.add_atom(Atom::new(Element::C));
1353 b.add_atom(Atom::new(Element::N));
1354 let mol = b.build();
1355 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1356 assert_eq!(neighbors.len(), 0);
1357 }
1358
1359 #[test]
1360 fn test_neighbors_opt_invalid() {
1361 let mol = ethane();
1362 assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1363 assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1364 }
1365
1366 #[test]
1367 fn test_degree_opt_valid() {
1368 let mol = ethane();
1369 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1370 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1371 }
1372
1373 #[test]
1374 fn test_degree_opt_isolated_atom() {
1375 let mut b = MoleculeBuilder::new();
1376 b.add_atom(Atom::new(Element::C));
1377 b.add_atom(Atom::new(Element::N));
1378 let mol = b.build();
1379 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1380 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1381 }
1382
1383 #[test]
1384 fn test_degree_opt_invalid() {
1385 let mol = ethane();
1386 assert!(mol.degree_opt(AtomIdx(2)).is_none());
1387 assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1388 }
1389
1390 #[test]
1391 fn test_degree_opt_multiple_bonds() {
1392 let mut b = MoleculeBuilder::new();
1394 let center = b.add_atom(Atom::new(Element::C));
1395 let n1 = b.add_atom(Atom::new(Element::C));
1396 let n2 = b.add_atom(Atom::new(Element::N));
1397 let n3 = b.add_atom(Atom::new(Element::O));
1398 b.add_bond(center, n1, BondOrder::Single).unwrap();
1399 b.add_bond(center, n2, BondOrder::Double).unwrap();
1400 b.add_bond(center, n3, BondOrder::Single).unwrap();
1401 let mol = b.build();
1402 assert_eq!(mol.degree_opt(center), Some(3));
1403 assert_eq!(mol.degree_opt(n1), Some(1));
1404 assert_eq!(mol.degree_opt(n2), Some(1));
1405 assert_eq!(mol.degree_opt(n3), Some(1));
1406 }
1407}