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 bond_direction_anchors: std::collections::HashMap<u32, AtomIdx>,
72}
73
74impl Molecule {
75 pub fn atom_count(&self) -> usize {
77 self.atoms.len()
78 }
79
80 pub fn bond_count(&self) -> usize {
82 self.bonds.len()
83 }
84
85 pub fn atom(&self, idx: AtomIdx) -> &Atom {
92 let i = idx.0 as usize;
93 if i >= self.atoms.len() {
94 panic!("atom index out of range");
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!("bond index out of range");
119 }
120 &self.bonds[i]
121 }
122
123 pub fn bond_opt(&self, idx: BondIdx) -> Option<&BondEntry> {
125 let i = idx.0 as usize;
126 if i < self.bonds.len() {
127 Some(&self.bonds[i])
128 } else {
129 None
130 }
131 }
132
133 pub fn atoms(&self) -> impl Iterator<Item = (AtomIdx, &Atom)> {
135 self.atoms
136 .iter()
137 .enumerate()
138 .map(|(i, a)| (AtomIdx(i as u32), a))
139 }
140
141 pub fn bonds(&self) -> impl Iterator<Item = (BondIdx, &BondEntry)> {
143 self.bonds
144 .iter()
145 .enumerate()
146 .map(|(i, b)| (BondIdx(i as u32), b))
147 }
148
149 pub fn neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (AtomIdx, BondIdx)> + '_ {
156 let i = idx.0 as usize;
157 if i >= self.adjacency.len() {
158 panic!("atom index out of range");
161 }
162 self.adjacency[i].iter().copied()
163 }
164
165 pub fn neighbors_opt(&self, idx: AtomIdx) -> Option<Vec<(AtomIdx, BondIdx)>> {
167 let i = idx.0 as usize;
168 if i < self.adjacency.len() {
169 Some(self.adjacency[i].to_vec())
170 } else {
171 None
172 }
173 }
174
175 pub fn degree(&self, idx: AtomIdx) -> usize {
182 let i = idx.0 as usize;
183 if i >= self.adjacency.len() {
184 panic!("atom index out of range");
185 }
186 self.adjacency[i].len()
187 }
188
189 pub fn degree_opt(&self, idx: AtomIdx) -> Option<usize> {
191 let i = idx.0 as usize;
192 if i < self.adjacency.len() {
193 Some(self.adjacency[i].len())
194 } else {
195 None
196 }
197 }
198
199 pub fn bond_between(&self, a: AtomIdx, b: AtomIdx) -> Option<(BondIdx, &BondEntry)> {
201 let a_idx = a.0 as usize;
202 let b_idx = b.0 as usize;
203 if a_idx >= self.adjacency.len() || b_idx >= self.atoms.len() {
204 return None;
205 }
206 self.adjacency[a_idx]
207 .iter()
208 .find(|&&(nb, _)| nb == b)
209 .and_then(|&(_, bidx)| {
210 let bond_idx = bidx.0 as usize;
211 if bond_idx < self.bonds.len() {
212 Some((bidx, &self.bonds[bond_idx]))
213 } else {
214 None
215 }
216 })
217 }
218
219 pub fn formula(&self) -> String {
221 use std::collections::BTreeMap;
222 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
223 for (_, atom) in self.atoms() {
224 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
225 }
226 let mut result = Self::format_hill_order_formula(&counts);
227 let total_charge: i32 = self.atoms().map(|(_, a)| a.charge as i32).sum();
228 match total_charge {
229 0 => {}
230 1 => result.push('+'),
231 -1 => result.push('-'),
232 n if n > 0 => result.push_str(&format!("+{n}")),
233 n => result.push_str(&n.to_string()),
234 }
235 result
236 }
237}
238
239impl Molecule {
244 fn format_hill_order_formula(counts: &std::collections::BTreeMap<&str, u32>) -> String {
246 let mut counts = counts.clone();
247 let mut result = String::new();
248 let push_count = |sym: &str, n: u32, out: &mut String| {
249 out.push_str(sym);
250 if n > 1 {
251 out.push_str(&n.to_string());
252 }
253 };
254 if let Some(c) = counts.remove("C") {
255 push_count("C", c, &mut result);
256 }
257 if let Some(h) = counts.remove("H")
258 && h > 0
259 {
260 push_count("H", h, &mut result);
261 }
262 for (sym, count) in &counts {
263 push_count(sym, *count, &mut result);
264 }
265 result
266 }
267
268 pub fn with_atom_added(&self, atom: Atom) -> (Molecule, AtomIdx) {
271 let mut builder = MoleculeBuilder::from_molecule(self);
272 let new_idx = builder.add_atom(atom);
273 (builder.build(), new_idx)
274 }
275
276 pub fn with_bond_added(
282 &self,
283 a: AtomIdx,
284 b: AtomIdx,
285 order: BondOrder,
286 ) -> Result<(Molecule, BondIdx), MolError> {
287 let mut builder = MoleculeBuilder::from_molecule(self);
288 let bond_idx = builder.add_bond(a, b, order)?;
289 Ok((builder.build(), bond_idx))
290 }
291
292 pub fn with_atom_charge(&self, idx: AtomIdx, charge: i8) -> Molecule {
294 let mut builder = MoleculeBuilder::new();
295 for (aidx, atom) in self.atoms() {
296 let mut a = atom.clone();
297 if aidx == idx {
298 a.charge = charge;
299 }
300 builder.add_atom(a);
301 }
302 for (_, bond) in self.bonds() {
303 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
304 }
305 builder.copy_stereo_from(self);
306 builder.copy_bond_directions_from(self);
307 builder.build()
308 }
309
310 pub fn with_atom_element(&self, idx: AtomIdx, el: Element) -> Molecule {
315 let mut builder = MoleculeBuilder::new();
316 for (aidx, atom) in self.atoms() {
317 let mut a = atom.clone();
318 if aidx == idx {
319 a.element = el;
320 a.chirality = crate::atom::Chirality::None;
322 a.hydrogen_count = None;
323 a.aromatic = false;
324 }
325 builder.add_atom(a);
326 }
327 for (_, bond) in self.bonds() {
328 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
329 }
330 builder.copy_stereo_from(self);
331 builder.copy_bond_directions_from(self);
332 builder.clear_stereo_neighbor_order(idx);
334 builder.build()
335 }
336
337 pub fn with_atom_removed(&self, idx: AtomIdx) -> (Molecule, Vec<Option<AtomIdx>>) {
344 let n = self.atom_count();
345 let removed = idx.0 as usize;
346
347 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
349 let mut new_pos = 0u32;
350 for (old, slot) in remap.iter_mut().enumerate() {
351 if old == removed {
352 continue;
353 }
354 *slot = Some(AtomIdx(new_pos));
355 new_pos += 1;
356 }
357
358 let mut builder = MoleculeBuilder::new();
359 for (aidx, atom) in self.atoms() {
360 if aidx == idx {
361 continue;
362 }
363 builder.add_atom(atom.clone());
364 }
365 let mut bond_remap: Vec<Option<BondIdx>> = vec![None; self.bonds.len()];
372 for (old_bidx, bond) in self.bonds() {
373 if bond.atom1 == idx || bond.atom2 == idx {
374 continue;
375 }
376 if let (Some(a1), Some(a2)) =
377 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
378 && let Ok(new_bidx) = builder.add_bond(a1, a2, bond.order)
379 {
380 bond_remap[old_bidx.0 as usize] = Some(new_bidx);
381 }
382 }
383 for (old_key, order) in &self.stereo_neighbor_order {
385 let old_atom = *old_key as usize;
386 if old_atom == removed {
387 continue; }
389 if let Some(Some(new_key)) = remap.get(old_atom) {
390 let new_order: Vec<u32> = order
391 .iter()
392 .filter_map(|&v| {
393 if v == STEREO_H_SENTINEL {
394 Some(STEREO_H_SENTINEL)
395 } else if v as usize == removed {
396 None } else {
398 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
399 }
400 })
401 .collect();
402 builder.set_stereo_neighbor_order(*new_key, new_order);
403 }
404 }
405 for (old_bidx, direction) in &self.bond_directions {
406 if let Some(Some(new_bidx)) = bond_remap.get(*old_bidx as usize) {
407 builder.set_bond_direction(*new_bidx, *direction);
408 }
409 }
410 (builder.build(), remap)
411 }
412
413 pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
417 crate::valence::implicit_hcount(self, idx)
418 }
419
420 pub fn total_formula(&self) -> String {
426 use std::collections::BTreeMap;
427 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
428 let mut implicit_h: u32 = 0;
429 for (aidx, atom) in self.atoms() {
430 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
431 implicit_h += crate::valence::implicit_hcount(self, aidx) as u32;
432 }
433 *counts.entry("H").or_insert(0) += implicit_h;
434 Self::format_hill_order_formula(&counts)
435 }
436
437 pub fn formula_with_isotopes(&self) -> String {
443 use std::collections::BTreeMap;
444 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
446 let mut has_carbon = false;
447 let mut has_explicit_h = false;
448 for (_, atom) in self.atoms() {
449 let sym = atom.element.symbol();
450 let key = match atom.isotope {
451 Some(n) => format!("{n}{sym}"),
452 None => sym.to_string(),
453 };
454 if sym == "C" && atom.isotope.is_none() {
455 has_carbon = true;
456 }
457 if sym == "H" {
458 has_explicit_h = true;
459 }
460 *counts.entry(key).or_insert(0) += 1;
461 }
462
463 let push_count = |key: &str, n: u32, out: &mut String| {
464 out.push_str(key);
465 if n > 1 {
466 out.push_str(&n.to_string());
467 }
468 };
469
470 let mut result = String::new();
471 if has_carbon && let Some(c) = counts.remove("C") {
473 push_count("C", c, &mut result);
474 }
475 if has_explicit_h && let Some(h) = counts.remove("H") {
476 push_count("H", h, &mut result);
477 }
478 for (key, count) in &counts {
479 push_count(key, *count, &mut result);
480 }
481 result
482 }
483
484 pub fn with_atom_aromatic(&self, idx: AtomIdx, aromatic: bool) -> Molecule {
486 let mut builder = MoleculeBuilder::new();
487 for (aidx, atom) in self.atoms() {
488 let mut a = atom.clone();
489 if aidx == idx {
490 a.aromatic = aromatic;
491 }
492 builder.add_atom(a);
493 }
494 for (_, bond) in self.bonds() {
495 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
496 }
497 builder.copy_stereo_from(self);
498 builder.copy_bond_directions_from(self);
499 builder.build()
500 }
501
502 pub fn with_bond_order(&self, idx: BondIdx, order: BondOrder) -> Molecule {
504 let mut builder = MoleculeBuilder::new();
505 for (_, atom) in self.atoms() {
506 builder.add_atom(atom.clone());
507 }
508 for (bidx, bond) in self.bonds() {
509 let o = if bidx == idx { order } else { bond.order };
510 let _ = builder.add_bond(bond.atom1, bond.atom2, o);
511 }
512 builder.copy_stereo_from(self);
513 builder.copy_bond_directions_from(self);
514 builder.build()
515 }
516
517 pub fn with_bond_removed(&self, idx: BondIdx) -> Molecule {
525 let mut builder = MoleculeBuilder::new();
526 for (_, atom) in self.atoms() {
527 builder.add_atom(atom.clone());
528 }
529 for (bidx, bond) in self.bonds() {
530 if bidx == idx {
531 continue;
532 }
533 if let Ok(new_bidx) = builder.add_bond(bond.atom1, bond.atom2, bond.order)
534 && let Some(direction) = self.bond_direction(bidx)
535 {
536 builder.set_bond_direction(new_bidx, direction);
537 }
538 }
539 builder.copy_stereo_from(self);
540 builder.build()
541 }
542}
543
544impl Molecule {
549 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
551 let idx = AtomIdx(self.atoms.len() as u32);
552 self.atoms.push(atom);
553 self.adjacency.push(vec![]);
554 idx
555 }
556
557 pub fn remove_atom(&mut self, idx: AtomIdx) -> Vec<Option<AtomIdx>> {
563 let n = self.atoms.len();
564 let removed = idx.0 as usize;
565
566 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
567 let mut new_pos = 0u32;
568 for (old, slot) in remap.iter_mut().enumerate() {
569 if old == removed {
570 continue;
571 }
572 *slot = Some(AtomIdx(new_pos));
573 new_pos += 1;
574 }
575
576 let _ = self.atoms.drain(removed..=removed).next();
582
583 let mut new_bonds: Vec<BondEntry> = Vec::new();
588 let mut bond_remap: Vec<Option<u32>> = vec![None; self.bonds.len()];
589 for (old_bidx, bond) in self.bonds.iter().enumerate() {
590 if bond.atom1 == idx || bond.atom2 == idx {
591 continue;
592 }
593 if let (Some(a1), Some(a2)) =
594 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
595 {
596 bond_remap[old_bidx] = Some(new_bonds.len() as u32);
597 new_bonds.push(BondEntry {
598 atom1: a1,
599 atom2: a2,
600 order: bond.order,
601 });
602 }
603 }
604 self.bonds = new_bonds;
605
606 let old_bond_directions = std::mem::take(&mut self.bond_directions);
608 for (old_key, direction) in old_bond_directions {
609 if let Some(Some(new_key)) = bond_remap.get(old_key as usize) {
610 self.bond_directions.insert(*new_key, direction);
611 }
612 }
613 let old_bond_anchors = std::mem::take(&mut self.bond_direction_anchors);
614 for (old_key, atom) in old_bond_anchors {
615 if let (Some(Some(new_key)), Some(new_atom)) = (
616 bond_remap.get(old_key as usize),
617 remap.get(atom.0 as usize).and_then(|r| *r),
618 ) {
619 self.bond_direction_anchors.insert(*new_key, new_atom);
620 }
621 }
622
623 let new_n = self.atoms.len();
625 self.adjacency = vec![vec![]; new_n];
626 for (bidx, bond) in self.bonds.iter().enumerate() {
627 let bi = BondIdx(bidx as u32);
628 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
629 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
630 }
631
632 let old_stereo = std::mem::take(&mut self.stereo_neighbor_order);
634 for (old_key, order) in old_stereo {
635 let old_atom = old_key as usize;
636 if old_atom == removed {
637 continue;
638 }
639 if let Some(Some(new_key)) = remap.get(old_atom) {
640 let new_order: Vec<u32> = order
641 .iter()
642 .filter_map(|&v| {
643 if v == STEREO_H_SENTINEL {
644 Some(STEREO_H_SENTINEL)
645 } else if v as usize == removed {
646 None
647 } else {
648 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
649 }
650 })
651 .collect();
652 self.stereo_neighbor_order.insert(new_key.0, new_order);
653 }
654 }
655
656 remap
657 }
658
659 pub fn add_bond(
663 &mut self,
664 a: AtomIdx,
665 b: AtomIdx,
666 order: BondOrder,
667 ) -> Result<BondIdx, MolError> {
668 let n = self.atoms.len() as u32;
669 if a.0 >= n {
670 return Err(MolError::InvalidAtomIdx(a));
671 }
672 if b.0 >= n {
673 return Err(MolError::InvalidAtomIdx(b));
674 }
675 if self.adjacency[a.0 as usize].iter().any(|&(nb, _)| nb == b) {
676 return Err(MolError::DuplicateBond(a, b));
677 }
678 let bidx = BondIdx(self.bonds.len() as u32);
679 self.bonds.push(BondEntry {
680 atom1: a,
681 atom2: b,
682 order,
683 });
684 self.adjacency[a.0 as usize].push((b, bidx));
685 self.adjacency[b.0 as usize].push((a, bidx));
686 Ok(bidx)
687 }
688
689 pub fn remove_bond(&mut self, idx: BondIdx) {
692 let removed = idx.0 as usize;
693 if removed >= self.bonds.len() {
694 return;
695 }
696 self.bonds.remove(removed);
697 let old_bond_directions = std::mem::take(&mut self.bond_directions);
706 for (old_key, direction) in old_bond_directions {
707 let old = old_key as usize;
708 match old.cmp(&removed) {
709 std::cmp::Ordering::Less => {
710 self.bond_directions.insert(old_key, direction);
711 }
712 std::cmp::Ordering::Equal => {} std::cmp::Ordering::Greater => {
714 self.bond_directions.insert(old_key - 1, direction);
715 }
716 }
717 }
718 let old_bond_anchors = std::mem::take(&mut self.bond_direction_anchors);
719 for (old_key, atom) in old_bond_anchors {
720 let old = old_key as usize;
721 let new_key = match old.cmp(&removed) {
722 std::cmp::Ordering::Less => old_key,
723 std::cmp::Ordering::Equal => continue,
724 std::cmp::Ordering::Greater => old_key - 1,
725 };
726 self.bond_direction_anchors.insert(new_key, atom);
727 }
728 let n = self.atoms.len();
730 self.adjacency = vec![vec![]; n];
731 for (bidx, bond) in self.bonds.iter().enumerate() {
732 let bi = BondIdx(bidx as u32);
733 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
734 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
735 }
736 }
737
738 pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
740 self.atoms[idx.0 as usize].charge = charge;
741 }
742
743 pub fn set_isotope(&mut self, idx: AtomIdx, isotope: Option<u16>) {
746 self.atoms[idx.0 as usize].isotope = isotope;
747 }
748
749 pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
753 let a = &mut self.atoms[idx.0 as usize];
754 a.element = el;
755 a.chirality = crate::atom::Chirality::None;
756 a.hydrogen_count = None;
757 a.aromatic = false;
758 }
759
760 pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
762 self.atoms[idx.0 as usize].cip_code = code;
763 }
764
765 pub fn set_chirality(&mut self, idx: AtomIdx, chirality: crate::atom::Chirality) {
767 self.atoms[idx.0 as usize].chirality = chirality;
768 }
769
770 pub fn set_bond_order(&mut self, idx: BondIdx, order: BondOrder) {
778 self.bonds[idx.0 as usize].order = order;
779 }
780
781 pub fn stereo_groups(&self) -> &[StereoGroup] {
783 &self.stereo_groups
784 }
785
786 pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
788 self.stereo_groups = groups;
789 }
790
791 pub fn add_stereo_group(&mut self, group: StereoGroup) {
793 self.stereo_groups.push(group);
794 }
795
796 pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
820 self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
821 }
822
823 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
825 self.stereo_neighbor_order.insert(idx.0, order);
826 }
827
828 pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
832 self.bond_directions.get(&idx.0).copied()
833 }
834
835 pub fn bond_direction_anchor(&self, idx: BondIdx) -> Option<AtomIdx> {
838 self.bond_direction_anchors.get(&idx.0).copied()
839 }
840
841 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
843 self.bond_directions.insert(idx.0, direction);
844 }
845
846 pub fn set_bond_direction_anchor(&mut self, idx: BondIdx, atom: AtomIdx) {
848 self.bond_direction_anchors.insert(idx.0, atom);
849 }
850}
851
852impl Molecule {
857 pub fn is_connected(&self) -> bool {
860 let n = self.atoms.len();
861 if n == 0 {
862 return true;
863 }
864 let mut visited = vec![false; n];
865 let mut stack = vec![AtomIdx(0)];
866 visited[0] = true;
867 let mut count = 1;
868 while let Some(cur) = stack.pop() {
869 for (nb, _) in self.neighbors(cur) {
870 if !visited[nb.0 as usize] {
871 visited[nb.0 as usize] = true;
872 count += 1;
873 stack.push(nb);
874 }
875 }
876 }
877 count == n
878 }
879
880 pub fn fragments(&self) -> Vec<Molecule> {
885 let n = self.atoms.len();
886 if n == 0 {
887 return vec![];
888 }
889
890 let mut component: Vec<usize> = vec![usize::MAX; n];
891 let mut comp_id = 0;
892
893 for start in 0..n {
894 if component[start] != usize::MAX {
895 continue;
896 }
897 let mut stack = vec![start];
898 component[start] = comp_id;
899 while let Some(cur) = stack.pop() {
900 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
901 let ni = nb.0 as usize;
902 if component[ni] == usize::MAX {
903 component[ni] = comp_id;
904 stack.push(ni);
905 }
906 }
907 }
908 comp_id += 1;
909 }
910
911 (0..comp_id)
912 .map(|cid| {
913 let mut builder = MoleculeBuilder::new();
914 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
915 std::collections::HashMap::new();
916 for (aidx, atom) in self.atoms() {
917 if component[aidx.0 as usize] == cid {
918 let new_idx = builder.add_atom(atom.clone());
919 old_to_new.insert(aidx, new_idx);
920 }
921 }
922 for (_, bond) in self.bonds() {
923 if let (Some(&a1), Some(&a2)) =
924 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
925 {
926 let _ = builder.add_bond(a1, a2, bond.order);
927 }
928 }
929 builder.build()
930 })
931 .collect()
932 }
933}
934
935#[derive(Default)]
939pub struct MoleculeBuilder {
940 atoms: Vec<Atom>,
941 bonds: Vec<BondEntry>,
942 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
943 stereo_groups: Vec<StereoGroup>,
944 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
945 bond_directions: std::collections::HashMap<u32, BondOrder>,
946 bond_direction_anchors: std::collections::HashMap<u32, AtomIdx>,
947}
948
949impl MoleculeBuilder {
950 pub fn new() -> Self {
951 Self::default()
952 }
953
954 pub fn with_capacity(atom_count: usize, bond_count: usize) -> Self {
960 Self {
961 atoms: Vec::with_capacity(atom_count),
962 bonds: Vec::with_capacity(bond_count),
963 adjacency: Vec::with_capacity(atom_count),
964 stereo_groups: Vec::new(),
965 stereo_neighbor_order: std::collections::HashMap::new(),
966 bond_directions: std::collections::HashMap::new(),
967 bond_direction_anchors: std::collections::HashMap::new(),
968 }
969 }
970
971 pub fn from_molecule(mol: &Molecule) -> Self {
976 let mut b = Self::new();
977 for (_, atom) in mol.atoms() {
978 b.add_atom(atom.clone());
979 }
980 for (_, bond) in mol.bonds() {
981 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
982 }
983 b.stereo_groups = mol.stereo_groups.clone();
984 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
985 b.bond_directions = mol.bond_directions.clone();
986 b.bond_direction_anchors = mol.bond_direction_anchors.clone();
987 b
988 }
989
990 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
992 self.stereo_neighbor_order.insert(idx.0, order);
993 }
994
995 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
997 self.stereo_neighbor_order.remove(&idx.0);
998 }
999
1000 pub fn add_stereo_group(&mut self, group: StereoGroup) {
1002 self.stereo_groups.push(group);
1003 }
1004
1005 pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
1011 self.stereo_groups = mol.stereo_groups.clone();
1012 }
1013
1014 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
1016 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
1017 }
1018
1019 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
1021 self.bond_directions.insert(idx.0, direction);
1022 }
1023
1024 pub fn set_bond_direction_anchor(&mut self, idx: BondIdx, atom: AtomIdx) {
1026 self.bond_direction_anchors.insert(idx.0, atom);
1027 }
1028
1029 pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
1037 self.bond_directions = mol.bond_directions.clone();
1038 self.bond_direction_anchors = mol.bond_direction_anchors.clone();
1039 }
1040
1041 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
1049 &self.atoms[idx.0 as usize]
1050 }
1051
1052 pub fn atom_count(&self) -> usize {
1054 self.atoms.len()
1055 }
1056
1057 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
1060 self.adjacency[idx.0 as usize]
1061 .iter()
1062 .map(|&(nb, bidx)| (bidx, nb))
1063 }
1064
1065 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
1067 let idx = AtomIdx(self.atoms.len() as u32);
1068 self.atoms.push(atom);
1069 self.adjacency.push(Vec::new());
1070 idx
1071 }
1072
1073 pub fn add_bond(
1077 &mut self,
1078 a: AtomIdx,
1079 b: AtomIdx,
1080 order: BondOrder,
1081 ) -> Result<BondIdx, MolError> {
1082 let n = self.atoms.len() as u32;
1083 if a.0 >= n {
1084 return Err(MolError::InvalidAtomIdx(a));
1085 }
1086 if b.0 >= n {
1087 return Err(MolError::InvalidAtomIdx(b));
1088 }
1089
1090 for &(nb, _) in &self.adjacency[a.0 as usize] {
1092 if nb == b {
1093 return Err(MolError::DuplicateBond(a, b));
1094 }
1095 }
1096
1097 let bidx = BondIdx(self.bonds.len() as u32);
1098 self.bonds.push(BondEntry {
1099 atom1: a,
1100 atom2: b,
1101 order,
1102 });
1103 self.adjacency[a.0 as usize].push((b, bidx));
1104 self.adjacency[b.0 as usize].push((a, bidx));
1105 Ok(bidx)
1106 }
1107
1108 pub fn build(self) -> Molecule {
1110 Molecule {
1111 atoms: self.atoms,
1112 bonds: self.bonds,
1113 adjacency: self.adjacency,
1114 stereo_groups: self.stereo_groups,
1115 stereo_neighbor_order: self.stereo_neighbor_order,
1116 bond_directions: self.bond_directions,
1117 bond_direction_anchors: self.bond_direction_anchors,
1118 }
1119 }
1120}
1121
1122#[cfg(test)]
1123mod tests {
1124 use super::*;
1125 use crate::atom::Atom;
1126 use crate::element::Element;
1127
1128 fn ethane() -> Molecule {
1129 let mut b = MoleculeBuilder::new();
1130 let c1 = b.add_atom(Atom::new(Element::C));
1131 let c2 = b.add_atom(Atom::new(Element::C));
1132 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1133 b.build()
1134 }
1135
1136 #[test]
1137 fn test_basic_molecule() {
1138 let mol = ethane();
1139 assert_eq!(mol.atom_count(), 2);
1140 assert_eq!(mol.bond_count(), 1);
1141 }
1142
1143 #[test]
1144 fn test_adjacency() {
1145 let mol = ethane();
1146 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1147 assert_eq!(neighbors.len(), 1);
1148 assert_eq!(neighbors[0].0, AtomIdx(1));
1149 }
1150
1151 #[test]
1152 fn test_bond_between() {
1153 let mol = ethane();
1154 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1155 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1156 }
1157
1158 #[test]
1159 fn test_duplicate_bond_error() {
1160 let mut b = MoleculeBuilder::new();
1161 let c1 = b.add_atom(Atom::new(Element::C));
1162 let c2 = b.add_atom(Atom::new(Element::C));
1163 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1164 let err = b.add_bond(c1, c2, BondOrder::Double);
1165 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1166 }
1167
1168 #[test]
1169 fn test_formula() {
1170 let mut b = MoleculeBuilder::new();
1171 let c = b.add_atom(Atom::new(Element::C));
1172 let n = b.add_atom(Atom::new(Element::N));
1173 b.add_bond(c, n, BondOrder::Single).unwrap();
1174 let mol = b.build();
1175 assert_eq!(mol.formula(), "CN");
1176 }
1177
1178 #[test]
1179 fn test_implicit_hydrogen_count() {
1180 let mut b = MoleculeBuilder::new();
1182 b.add_atom(Atom::organic(Element::C));
1183 let mol = b.build();
1184 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1185 }
1186
1187 #[test]
1188 fn test_total_formula_methane() {
1189 let mut b = MoleculeBuilder::new();
1191 b.add_atom(Atom::organic(Element::C));
1192 let mol = b.build();
1193 assert_eq!(mol.total_formula(), "CH4");
1194 }
1195
1196 #[test]
1197 fn test_total_formula_no_hydrogen() {
1198 let mut b = MoleculeBuilder::new();
1200 let na = b.add_atom(Atom::new(Element::NA));
1201 let cl = b.add_atom(Atom::new(Element::CL));
1202 b.add_bond(na, cl, BondOrder::Single).unwrap();
1203 let mol = b.build();
1204 assert_eq!(mol.total_formula(), "ClNa");
1205 }
1206
1207 #[test]
1208 fn test_with_atom_aromatic() {
1209 let mol = ethane();
1210 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1211 assert!(updated.atom(AtomIdx(0)).aromatic);
1212 assert!(!updated.atom(AtomIdx(1)).aromatic);
1213 }
1214
1215 #[test]
1216 fn test_with_bond_order() {
1217 let mol = ethane();
1218 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1219 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1220 }
1221
1222 fn chain_with_direction_on_last_bond() -> (Molecule, BondIdx) {
1230 let mut b = MoleculeBuilder::new();
1231 let a = b.add_atom(Atom::new(Element::C));
1232 let bb = b.add_atom(Atom::new(Element::C));
1233 let c = b.add_atom(Atom::new(Element::C));
1234 let d = b.add_atom(Atom::new(Element::C));
1235 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);
1239 b.set_bond_direction_anchor(cd, c);
1240 (b.build(), cd)
1241 }
1242
1243 #[test]
1244 fn test_remove_bond_remaps_bond_direction_not_misattributes() {
1245 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1246 assert_eq!(mol.bond_count(), 3);
1247 mol.remove_bond(BondIdx(0)); assert_eq!(mol.bond_count(), 2);
1249 assert_eq!(mol.bond_direction(BondIdx(1)), Some(BondOrder::Up));
1251 assert_eq!(mol.bond_direction_anchor(BondIdx(1)), Some(AtomIdx(2)));
1252 assert_eq!(mol.bond_direction(BondIdx(0)), None);
1256 assert_eq!(mol.bond_opt(BondIdx(2)), None);
1257 }
1258
1259 #[test]
1260 fn test_remove_bond_drops_direction_for_the_removed_bond_itself() {
1261 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1262 mol.remove_bond(BondIdx(2)); assert_eq!(mol.bond_count(), 2);
1264 assert!(mol.bond_direction(BondIdx(0)).is_none());
1265 assert!(mol.bond_direction(BondIdx(1)).is_none());
1266 }
1267
1268 #[test]
1269 fn test_with_atom_removed_remaps_bond_direction() {
1270 let (mol, _cd) = chain_with_direction_on_last_bond();
1271 let (updated, _atom_remap) = mol.with_atom_removed(AtomIdx(0));
1275 assert_eq!(updated.bond_count(), 2);
1276 let has_direction = (0..updated.bond_count())
1280 .map(|i| BondIdx(i as u32))
1281 .any(|bidx| updated.bond_direction(bidx) == Some(BondOrder::Up));
1282 assert!(
1283 has_direction,
1284 "bond_direction on C-D must survive atom removal, remapped to its new bond index"
1285 );
1286 }
1287
1288 #[test]
1291 fn test_add_remove_atom() {
1292 let mut mol = ethane();
1293 let n_idx = mol.add_atom(Atom::new(Element::N));
1294 assert_eq!(mol.atom_count(), 3);
1295 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1296
1297 let remap = mol.remove_atom(n_idx);
1298 assert_eq!(mol.atom_count(), 2);
1299 assert!(remap[n_idx.0 as usize].is_none());
1300 }
1301
1302 #[test]
1303 fn test_remove_atom_preserves_survivor_order() {
1304 let mut mol = MoleculeBuilder::new().build();
1305 mol.add_atom(Atom::new(Element::C));
1306 let removed = mol.add_atom(Atom::new(Element::N));
1307 mol.add_atom(Atom::new(Element::O));
1308
1309 let remap = mol.remove_atom(removed);
1310
1311 assert_eq!(
1312 mol.atoms()
1313 .map(|(_, atom)| atom.element)
1314 .collect::<Vec<_>>(),
1315 vec![Element::C, Element::O]
1316 );
1317 assert_eq!(remap, vec![Some(AtomIdx(0)), None, Some(AtomIdx(1))]);
1318 }
1319
1320 #[test]
1321 fn test_add_remove_bond() {
1322 let mut mol = ethane();
1323 let n_idx = mol.add_atom(Atom::new(Element::N));
1324 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1325 assert_eq!(mol.bond_count(), 2);
1326 mol.remove_bond(bidx);
1327 assert_eq!(mol.bond_count(), 1);
1328 }
1329
1330 #[test]
1331 fn test_set_charge_element() {
1332 let mut mol = ethane();
1333 mol.set_charge(AtomIdx(0), 1);
1334 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1335 mol.set_element(AtomIdx(0), Element::N);
1336 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1337 }
1338
1339 #[test]
1340 fn test_is_connected() {
1341 let mol = ethane();
1342 assert!(mol.is_connected());
1343
1344 let mut b = MoleculeBuilder::new();
1346 b.add_atom(Atom::new(Element::C));
1347 b.add_atom(Atom::new(Element::N));
1348 let disconnected = b.build();
1349 assert!(!disconnected.is_connected());
1350 }
1351
1352 #[test]
1353 fn test_fragments() {
1354 let mut b = MoleculeBuilder::new();
1356 let c1 = b.add_atom(Atom::organic(Element::C));
1357 let c2 = b.add_atom(Atom::organic(Element::C));
1358 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1359 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1361 let frags = mol.fragments();
1362 assert_eq!(frags.len(), 2);
1363 let sizes: std::collections::HashSet<usize> =
1364 frags.iter().map(|f| f.atom_count()).collect();
1365 assert!(sizes.contains(&2));
1366 assert!(sizes.contains(&1));
1367 }
1368
1369 #[test]
1370 fn test_builder_from_molecule() {
1371 let mol = ethane();
1372 let mut b = MoleculeBuilder::from_molecule(&mol);
1373 b.add_atom(Atom::new(Element::O));
1374 let mol2 = b.build();
1375 assert_eq!(mol2.atom_count(), 3);
1376 assert_eq!(mol2.bond_count(), 1); }
1378
1379 #[test]
1382 fn test_atom_opt_valid() {
1383 let mol = ethane();
1384 assert!(mol.atom_opt(AtomIdx(0)).is_some());
1385 assert!(mol.atom_opt(AtomIdx(1)).is_some());
1386 let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1387 assert_eq!(atom.element.atomic_number(), 6);
1388 }
1389
1390 #[test]
1391 fn test_atom_opt_invalid() {
1392 let mol = ethane();
1393 assert!(mol.atom_opt(AtomIdx(2)).is_none());
1394 assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1395 }
1396
1397 #[test]
1398 fn test_bond_opt_valid() {
1399 let mol = ethane();
1400 assert!(mol.bond_opt(BondIdx(0)).is_some());
1401 let bond = mol.bond_opt(BondIdx(0)).unwrap();
1402 assert_eq!(bond.order, BondOrder::Single);
1403 }
1404
1405 #[test]
1406 fn test_bond_opt_invalid() {
1407 let mol = ethane();
1408 assert!(mol.bond_opt(BondIdx(1)).is_none());
1409 assert!(mol.bond_opt(BondIdx(1000)).is_none());
1410 }
1411
1412 #[test]
1413 fn test_neighbors_opt_valid() {
1414 let mol = ethane();
1415 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1416 assert_eq!(neighbors.len(), 1);
1417 assert_eq!(neighbors[0].0, AtomIdx(1));
1418 }
1419
1420 #[test]
1421 fn test_neighbors_opt_isolated_atom() {
1422 let mut b = MoleculeBuilder::new();
1423 b.add_atom(Atom::new(Element::C));
1424 b.add_atom(Atom::new(Element::N));
1425 let mol = b.build();
1426 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1427 assert_eq!(neighbors.len(), 0);
1428 }
1429
1430 #[test]
1431 fn test_neighbors_opt_invalid() {
1432 let mol = ethane();
1433 assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1434 assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1435 }
1436
1437 #[test]
1438 fn test_degree_opt_valid() {
1439 let mol = ethane();
1440 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1441 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1442 }
1443
1444 #[test]
1445 fn test_degree_opt_isolated_atom() {
1446 let mut b = MoleculeBuilder::new();
1447 b.add_atom(Atom::new(Element::C));
1448 b.add_atom(Atom::new(Element::N));
1449 let mol = b.build();
1450 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1451 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1452 }
1453
1454 #[test]
1455 fn test_degree_opt_invalid() {
1456 let mol = ethane();
1457 assert!(mol.degree_opt(AtomIdx(2)).is_none());
1458 assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1459 }
1460
1461 #[test]
1462 fn test_degree_opt_multiple_bonds() {
1463 let mut b = MoleculeBuilder::new();
1465 let center = b.add_atom(Atom::new(Element::C));
1466 let n1 = b.add_atom(Atom::new(Element::C));
1467 let n2 = b.add_atom(Atom::new(Element::N));
1468 let n3 = b.add_atom(Atom::new(Element::O));
1469 b.add_bond(center, n1, BondOrder::Single).unwrap();
1470 b.add_bond(center, n2, BondOrder::Double).unwrap();
1471 b.add_bond(center, n3, BondOrder::Single).unwrap();
1472 let mol = b.build();
1473 assert_eq!(mol.degree_opt(center), Some(3));
1474 assert_eq!(mol.degree_opt(n1), Some(1));
1475 assert_eq!(mol.degree_opt(n2), Some(1));
1476 assert_eq!(mol.degree_opt(n3), Some(1));
1477 }
1478}