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!("atom index out of range");
91 }
92 &self.atoms[i]
93 }
94
95 pub fn atom_opt(&self, idx: AtomIdx) -> Option<&Atom> {
97 let i = idx.0 as usize;
98 if i < self.atoms.len() {
99 Some(&self.atoms[i])
100 } else {
101 None
102 }
103 }
104
105 pub fn bond(&self, idx: BondIdx) -> &BondEntry {
112 let i = idx.0 as usize;
113 if i >= self.bonds.len() {
114 panic!("bond index out of range");
115 }
116 &self.bonds[i]
117 }
118
119 pub fn bond_opt(&self, idx: BondIdx) -> Option<&BondEntry> {
121 let i = idx.0 as usize;
122 if i < self.bonds.len() {
123 Some(&self.bonds[i])
124 } else {
125 None
126 }
127 }
128
129 pub fn atoms(&self) -> impl Iterator<Item = (AtomIdx, &Atom)> {
131 self.atoms
132 .iter()
133 .enumerate()
134 .map(|(i, a)| (AtomIdx(i as u32), a))
135 }
136
137 pub fn bonds(&self) -> impl Iterator<Item = (BondIdx, &BondEntry)> {
139 self.bonds
140 .iter()
141 .enumerate()
142 .map(|(i, b)| (BondIdx(i as u32), b))
143 }
144
145 pub fn neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (AtomIdx, BondIdx)> + '_ {
152 let i = idx.0 as usize;
153 if i >= self.adjacency.len() {
154 panic!("atom index out of range");
157 }
158 self.adjacency[i].iter().copied()
159 }
160
161 pub fn neighbors_opt(&self, idx: AtomIdx) -> Option<Vec<(AtomIdx, BondIdx)>> {
163 let i = idx.0 as usize;
164 if i < self.adjacency.len() {
165 Some(self.adjacency[i].to_vec())
166 } else {
167 None
168 }
169 }
170
171 pub fn degree(&self, idx: AtomIdx) -> usize {
178 let i = idx.0 as usize;
179 if i >= self.adjacency.len() {
180 panic!("atom index out of range");
181 }
182 self.adjacency[i].len()
183 }
184
185 pub fn degree_opt(&self, idx: AtomIdx) -> Option<usize> {
187 let i = idx.0 as usize;
188 if i < self.adjacency.len() {
189 Some(self.adjacency[i].len())
190 } else {
191 None
192 }
193 }
194
195 pub fn bond_between(&self, a: AtomIdx, b: AtomIdx) -> Option<(BondIdx, &BondEntry)> {
197 let a_idx = a.0 as usize;
198 let b_idx = b.0 as usize;
199 if a_idx >= self.adjacency.len() || b_idx >= self.atoms.len() {
200 return None;
201 }
202 self.adjacency[a_idx]
203 .iter()
204 .find(|&&(nb, _)| nb == b)
205 .and_then(|&(_, bidx)| {
206 let bond_idx = bidx.0 as usize;
207 if bond_idx < self.bonds.len() {
208 Some((bidx, &self.bonds[bond_idx]))
209 } else {
210 None
211 }
212 })
213 }
214
215 pub fn formula(&self) -> String {
217 use std::collections::BTreeMap;
218 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
219 for (_, atom) in self.atoms() {
220 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
221 }
222 let mut result = Self::format_hill_order_formula(&counts);
223 let total_charge: i32 = self.atoms().map(|(_, a)| a.charge as i32).sum();
224 match total_charge {
225 0 => {}
226 1 => result.push('+'),
227 -1 => result.push('-'),
228 n if n > 0 => result.push_str(&format!("+{n}")),
229 n => result.push_str(&n.to_string()),
230 }
231 result
232 }
233}
234
235impl Molecule {
240 fn format_hill_order_formula(counts: &std::collections::BTreeMap<&str, u32>) -> String {
242 let mut counts = counts.clone();
243 let mut result = String::new();
244 let push_count = |sym: &str, n: u32, out: &mut String| {
245 out.push_str(sym);
246 if n > 1 {
247 out.push_str(&n.to_string());
248 }
249 };
250 if let Some(c) = counts.remove("C") {
251 push_count("C", c, &mut result);
252 }
253 if let Some(h) = counts.remove("H")
254 && h > 0
255 {
256 push_count("H", h, &mut result);
257 }
258 for (sym, count) in &counts {
259 push_count(sym, *count, &mut result);
260 }
261 result
262 }
263
264 pub fn with_atom_added(&self, atom: Atom) -> (Molecule, AtomIdx) {
267 let mut builder = MoleculeBuilder::from_molecule(self);
268 let new_idx = builder.add_atom(atom);
269 (builder.build(), new_idx)
270 }
271
272 pub fn with_bond_added(
278 &self,
279 a: AtomIdx,
280 b: AtomIdx,
281 order: BondOrder,
282 ) -> Result<(Molecule, BondIdx), MolError> {
283 let mut builder = MoleculeBuilder::from_molecule(self);
284 let bond_idx = builder.add_bond(a, b, order)?;
285 Ok((builder.build(), bond_idx))
286 }
287
288 pub fn with_atom_charge(&self, idx: AtomIdx, charge: i8) -> Molecule {
290 let mut builder = MoleculeBuilder::new();
291 for (aidx, atom) in self.atoms() {
292 let mut a = atom.clone();
293 if aidx == idx {
294 a.charge = charge;
295 }
296 builder.add_atom(a);
297 }
298 for (_, bond) in self.bonds() {
299 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
300 }
301 builder.copy_stereo_from(self);
302 builder.copy_bond_directions_from(self);
303 builder.build()
304 }
305
306 pub fn with_atom_element(&self, idx: AtomIdx, el: Element) -> Molecule {
311 let mut builder = MoleculeBuilder::new();
312 for (aidx, atom) in self.atoms() {
313 let mut a = atom.clone();
314 if aidx == idx {
315 a.element = el;
316 a.chirality = crate::atom::Chirality::None;
318 a.hydrogen_count = None;
319 a.aromatic = false;
320 }
321 builder.add_atom(a);
322 }
323 for (_, bond) in self.bonds() {
324 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
325 }
326 builder.copy_stereo_from(self);
327 builder.copy_bond_directions_from(self);
328 builder.clear_stereo_neighbor_order(idx);
330 builder.build()
331 }
332
333 pub fn with_atom_removed(&self, idx: AtomIdx) -> (Molecule, Vec<Option<AtomIdx>>) {
340 let n = self.atom_count();
341 let removed = idx.0 as usize;
342
343 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
345 let mut new_pos = 0u32;
346 for (old, slot) in remap.iter_mut().enumerate() {
347 if old == removed {
348 continue;
349 }
350 *slot = Some(AtomIdx(new_pos));
351 new_pos += 1;
352 }
353
354 let mut builder = MoleculeBuilder::new();
355 for (aidx, atom) in self.atoms() {
356 if aidx == idx {
357 continue;
358 }
359 builder.add_atom(atom.clone());
360 }
361 let mut bond_remap: Vec<Option<BondIdx>> = vec![None; self.bonds.len()];
368 for (old_bidx, bond) in self.bonds() {
369 if bond.atom1 == idx || bond.atom2 == idx {
370 continue;
371 }
372 if let (Some(a1), Some(a2)) =
373 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
374 && let Ok(new_bidx) = builder.add_bond(a1, a2, bond.order)
375 {
376 bond_remap[old_bidx.0 as usize] = Some(new_bidx);
377 }
378 }
379 for (old_key, order) in &self.stereo_neighbor_order {
381 let old_atom = *old_key as usize;
382 if old_atom == removed {
383 continue; }
385 if let Some(Some(new_key)) = remap.get(old_atom) {
386 let new_order: Vec<u32> = order
387 .iter()
388 .filter_map(|&v| {
389 if v == STEREO_H_SENTINEL {
390 Some(STEREO_H_SENTINEL)
391 } else if v as usize == removed {
392 None } else {
394 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
395 }
396 })
397 .collect();
398 builder.set_stereo_neighbor_order(*new_key, new_order);
399 }
400 }
401 for (old_bidx, direction) in &self.bond_directions {
402 if let Some(Some(new_bidx)) = bond_remap.get(*old_bidx as usize) {
403 builder.set_bond_direction(*new_bidx, *direction);
404 }
405 }
406 (builder.build(), remap)
407 }
408
409 pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
413 crate::valence::implicit_hcount(self, idx)
414 }
415
416 pub fn total_formula(&self) -> String {
422 use std::collections::BTreeMap;
423 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
424 let mut implicit_h: u32 = 0;
425 for (aidx, atom) in self.atoms() {
426 *counts.entry(atom.element.symbol()).or_insert(0) += 1;
427 implicit_h += crate::valence::implicit_hcount(self, aidx) as u32;
428 }
429 *counts.entry("H").or_insert(0) += implicit_h;
430 Self::format_hill_order_formula(&counts)
431 }
432
433 pub fn formula_with_isotopes(&self) -> String {
439 use std::collections::BTreeMap;
440 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
442 let mut has_carbon = false;
443 let mut has_explicit_h = false;
444 for (_, atom) in self.atoms() {
445 let sym = atom.element.symbol();
446 let key = match atom.isotope {
447 Some(n) => format!("{n}{sym}"),
448 None => sym.to_string(),
449 };
450 if sym == "C" && atom.isotope.is_none() {
451 has_carbon = true;
452 }
453 if sym == "H" {
454 has_explicit_h = true;
455 }
456 *counts.entry(key).or_insert(0) += 1;
457 }
458
459 let push_count = |key: &str, n: u32, out: &mut String| {
460 out.push_str(key);
461 if n > 1 {
462 out.push_str(&n.to_string());
463 }
464 };
465
466 let mut result = String::new();
467 if has_carbon && let Some(c) = counts.remove("C") {
469 push_count("C", c, &mut result);
470 }
471 if has_explicit_h && let Some(h) = counts.remove("H") {
472 push_count("H", h, &mut result);
473 }
474 for (key, count) in &counts {
475 push_count(key, *count, &mut result);
476 }
477 result
478 }
479
480 pub fn with_atom_aromatic(&self, idx: AtomIdx, aromatic: bool) -> Molecule {
482 let mut builder = MoleculeBuilder::new();
483 for (aidx, atom) in self.atoms() {
484 let mut a = atom.clone();
485 if aidx == idx {
486 a.aromatic = aromatic;
487 }
488 builder.add_atom(a);
489 }
490 for (_, bond) in self.bonds() {
491 let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
492 }
493 builder.copy_stereo_from(self);
494 builder.copy_bond_directions_from(self);
495 builder.build()
496 }
497
498 pub fn with_bond_order(&self, idx: BondIdx, order: BondOrder) -> Molecule {
500 let mut builder = MoleculeBuilder::new();
501 for (_, atom) in self.atoms() {
502 builder.add_atom(atom.clone());
503 }
504 for (bidx, bond) in self.bonds() {
505 let o = if bidx == idx { order } else { bond.order };
506 let _ = builder.add_bond(bond.atom1, bond.atom2, o);
507 }
508 builder.copy_stereo_from(self);
509 builder.copy_bond_directions_from(self);
510 builder.build()
511 }
512
513 pub fn with_bond_removed(&self, idx: BondIdx) -> Molecule {
521 let mut builder = MoleculeBuilder::new();
522 for (_, atom) in self.atoms() {
523 builder.add_atom(atom.clone());
524 }
525 for (bidx, bond) in self.bonds() {
526 if bidx == idx {
527 continue;
528 }
529 if let Ok(new_bidx) = builder.add_bond(bond.atom1, bond.atom2, bond.order)
530 && let Some(direction) = self.bond_direction(bidx)
531 {
532 builder.set_bond_direction(new_bidx, direction);
533 }
534 }
535 builder.copy_stereo_from(self);
536 builder.build()
537 }
538}
539
540impl Molecule {
545 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
547 let idx = AtomIdx(self.atoms.len() as u32);
548 self.atoms.push(atom);
549 self.adjacency.push(vec![]);
550 idx
551 }
552
553 pub fn remove_atom(&mut self, idx: AtomIdx) -> Vec<Option<AtomIdx>> {
559 let n = self.atoms.len();
560 let removed = idx.0 as usize;
561
562 let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
563 let mut new_pos = 0u32;
564 for (old, slot) in remap.iter_mut().enumerate() {
565 if old == removed {
566 continue;
567 }
568 *slot = Some(AtomIdx(new_pos));
569 new_pos += 1;
570 }
571
572 self.atoms.remove(removed);
573
574 let mut new_bonds: Vec<BondEntry> = Vec::new();
579 let mut bond_remap: Vec<Option<u32>> = vec![None; self.bonds.len()];
580 for (old_bidx, bond) in self.bonds.iter().enumerate() {
581 if bond.atom1 == idx || bond.atom2 == idx {
582 continue;
583 }
584 if let (Some(a1), Some(a2)) =
585 (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
586 {
587 bond_remap[old_bidx] = Some(new_bonds.len() as u32);
588 new_bonds.push(BondEntry {
589 atom1: a1,
590 atom2: a2,
591 order: bond.order,
592 });
593 }
594 }
595 self.bonds = new_bonds;
596
597 let old_bond_directions = std::mem::take(&mut self.bond_directions);
599 for (old_key, direction) in old_bond_directions {
600 if let Some(Some(new_key)) = bond_remap.get(old_key as usize) {
601 self.bond_directions.insert(*new_key, direction);
602 }
603 }
604
605 let new_n = self.atoms.len();
607 self.adjacency = vec![vec![]; new_n];
608 for (bidx, bond) in self.bonds.iter().enumerate() {
609 let bi = BondIdx(bidx as u32);
610 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
611 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
612 }
613
614 let old_stereo = std::mem::take(&mut self.stereo_neighbor_order);
616 for (old_key, order) in old_stereo {
617 let old_atom = old_key as usize;
618 if old_atom == removed {
619 continue;
620 }
621 if let Some(Some(new_key)) = remap.get(old_atom) {
622 let new_order: Vec<u32> = order
623 .iter()
624 .filter_map(|&v| {
625 if v == STEREO_H_SENTINEL {
626 Some(STEREO_H_SENTINEL)
627 } else if v as usize == removed {
628 None
629 } else {
630 remap.get(v as usize).and_then(|r| r.map(|a| a.0))
631 }
632 })
633 .collect();
634 self.stereo_neighbor_order.insert(new_key.0, new_order);
635 }
636 }
637
638 remap
639 }
640
641 pub fn add_bond(
645 &mut self,
646 a: AtomIdx,
647 b: AtomIdx,
648 order: BondOrder,
649 ) -> Result<BondIdx, MolError> {
650 let n = self.atoms.len() as u32;
651 if a.0 >= n {
652 return Err(MolError::InvalidAtomIdx(a));
653 }
654 if b.0 >= n {
655 return Err(MolError::InvalidAtomIdx(b));
656 }
657 if self.adjacency[a.0 as usize].iter().any(|&(nb, _)| nb == b) {
658 return Err(MolError::DuplicateBond(a, b));
659 }
660 let bidx = BondIdx(self.bonds.len() as u32);
661 self.bonds.push(BondEntry {
662 atom1: a,
663 atom2: b,
664 order,
665 });
666 self.adjacency[a.0 as usize].push((b, bidx));
667 self.adjacency[b.0 as usize].push((a, bidx));
668 Ok(bidx)
669 }
670
671 pub fn remove_bond(&mut self, idx: BondIdx) {
674 let removed = idx.0 as usize;
675 if removed >= self.bonds.len() {
676 return;
677 }
678 self.bonds.remove(removed);
679 let old_bond_directions = std::mem::take(&mut self.bond_directions);
688 for (old_key, direction) in old_bond_directions {
689 let old = old_key as usize;
690 match old.cmp(&removed) {
691 std::cmp::Ordering::Less => {
692 self.bond_directions.insert(old_key, direction);
693 }
694 std::cmp::Ordering::Equal => {} std::cmp::Ordering::Greater => {
696 self.bond_directions.insert(old_key - 1, direction);
697 }
698 }
699 }
700 let n = self.atoms.len();
702 self.adjacency = vec![vec![]; n];
703 for (bidx, bond) in self.bonds.iter().enumerate() {
704 let bi = BondIdx(bidx as u32);
705 self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
706 self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
707 }
708 }
709
710 pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
712 self.atoms[idx.0 as usize].charge = charge;
713 }
714
715 pub fn set_isotope(&mut self, idx: AtomIdx, isotope: Option<u16>) {
718 self.atoms[idx.0 as usize].isotope = isotope;
719 }
720
721 pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
725 let a = &mut self.atoms[idx.0 as usize];
726 a.element = el;
727 a.chirality = crate::atom::Chirality::None;
728 a.hydrogen_count = None;
729 a.aromatic = false;
730 }
731
732 pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
734 self.atoms[idx.0 as usize].cip_code = code;
735 }
736
737 pub fn set_chirality(&mut self, idx: AtomIdx, chirality: crate::atom::Chirality) {
739 self.atoms[idx.0 as usize].chirality = chirality;
740 }
741
742 pub fn set_bond_order(&mut self, idx: BondIdx, order: BondOrder) {
750 self.bonds[idx.0 as usize].order = order;
751 }
752
753 pub fn stereo_groups(&self) -> &[StereoGroup] {
755 &self.stereo_groups
756 }
757
758 pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
760 self.stereo_groups = groups;
761 }
762
763 pub fn add_stereo_group(&mut self, group: StereoGroup) {
765 self.stereo_groups.push(group);
766 }
767
768 pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
792 self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
793 }
794
795 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
797 self.stereo_neighbor_order.insert(idx.0, order);
798 }
799
800 pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
804 self.bond_directions.get(&idx.0).copied()
805 }
806
807 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
809 self.bond_directions.insert(idx.0, direction);
810 }
811}
812
813impl Molecule {
818 pub fn is_connected(&self) -> bool {
821 let n = self.atoms.len();
822 if n == 0 {
823 return true;
824 }
825 let mut visited = vec![false; n];
826 let mut stack = vec![AtomIdx(0)];
827 visited[0] = true;
828 let mut count = 1;
829 while let Some(cur) = stack.pop() {
830 for (nb, _) in self.neighbors(cur) {
831 if !visited[nb.0 as usize] {
832 visited[nb.0 as usize] = true;
833 count += 1;
834 stack.push(nb);
835 }
836 }
837 }
838 count == n
839 }
840
841 pub fn fragments(&self) -> Vec<Molecule> {
846 let n = self.atoms.len();
847 if n == 0 {
848 return vec![];
849 }
850
851 let mut component: Vec<usize> = vec![usize::MAX; n];
852 let mut comp_id = 0;
853
854 for start in 0..n {
855 if component[start] != usize::MAX {
856 continue;
857 }
858 let mut stack = vec![start];
859 component[start] = comp_id;
860 while let Some(cur) = stack.pop() {
861 for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
862 let ni = nb.0 as usize;
863 if component[ni] == usize::MAX {
864 component[ni] = comp_id;
865 stack.push(ni);
866 }
867 }
868 }
869 comp_id += 1;
870 }
871
872 (0..comp_id)
873 .map(|cid| {
874 let mut builder = MoleculeBuilder::new();
875 let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
876 std::collections::HashMap::new();
877 for (aidx, atom) in self.atoms() {
878 if component[aidx.0 as usize] == cid {
879 let new_idx = builder.add_atom(atom.clone());
880 old_to_new.insert(aidx, new_idx);
881 }
882 }
883 for (_, bond) in self.bonds() {
884 if let (Some(&a1), Some(&a2)) =
885 (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
886 {
887 let _ = builder.add_bond(a1, a2, bond.order);
888 }
889 }
890 builder.build()
891 })
892 .collect()
893 }
894}
895
896#[derive(Default)]
900pub struct MoleculeBuilder {
901 atoms: Vec<Atom>,
902 bonds: Vec<BondEntry>,
903 adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
904 stereo_groups: Vec<StereoGroup>,
905 stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
906 bond_directions: std::collections::HashMap<u32, BondOrder>,
907}
908
909impl MoleculeBuilder {
910 pub fn new() -> Self {
911 Self::default()
912 }
913
914 pub fn with_capacity(atom_count: usize, bond_count: usize) -> Self {
920 Self {
921 atoms: Vec::with_capacity(atom_count),
922 bonds: Vec::with_capacity(bond_count),
923 adjacency: Vec::with_capacity(atom_count),
924 stereo_groups: Vec::new(),
925 stereo_neighbor_order: std::collections::HashMap::new(),
926 bond_directions: std::collections::HashMap::new(),
927 }
928 }
929
930 pub fn from_molecule(mol: &Molecule) -> Self {
935 let mut b = Self::new();
936 for (_, atom) in mol.atoms() {
937 b.add_atom(atom.clone());
938 }
939 for (_, bond) in mol.bonds() {
940 let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
941 }
942 b.stereo_groups = mol.stereo_groups.clone();
943 b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
944 b.bond_directions = mol.bond_directions.clone();
945 b
946 }
947
948 pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
950 self.stereo_neighbor_order.insert(idx.0, order);
951 }
952
953 pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
955 self.stereo_neighbor_order.remove(&idx.0);
956 }
957
958 pub fn add_stereo_group(&mut self, group: StereoGroup) {
960 self.stereo_groups.push(group);
961 }
962
963 pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
969 self.stereo_groups = mol.stereo_groups.clone();
970 }
971
972 pub fn copy_stereo_from(&mut self, mol: &Molecule) {
974 self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
975 }
976
977 pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
979 self.bond_directions.insert(idx.0, direction);
980 }
981
982 pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
990 self.bond_directions = mol.bond_directions.clone();
991 }
992
993 pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
1001 &self.atoms[idx.0 as usize]
1002 }
1003
1004 pub fn atom_count(&self) -> usize {
1006 self.atoms.len()
1007 }
1008
1009 pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
1012 self.adjacency[idx.0 as usize]
1013 .iter()
1014 .map(|&(nb, bidx)| (bidx, nb))
1015 }
1016
1017 pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
1019 let idx = AtomIdx(self.atoms.len() as u32);
1020 self.atoms.push(atom);
1021 self.adjacency.push(Vec::new());
1022 idx
1023 }
1024
1025 pub fn add_bond(
1029 &mut self,
1030 a: AtomIdx,
1031 b: AtomIdx,
1032 order: BondOrder,
1033 ) -> Result<BondIdx, MolError> {
1034 let n = self.atoms.len() as u32;
1035 if a.0 >= n {
1036 return Err(MolError::InvalidAtomIdx(a));
1037 }
1038 if b.0 >= n {
1039 return Err(MolError::InvalidAtomIdx(b));
1040 }
1041
1042 for &(nb, _) in &self.adjacency[a.0 as usize] {
1044 if nb == b {
1045 return Err(MolError::DuplicateBond(a, b));
1046 }
1047 }
1048
1049 let bidx = BondIdx(self.bonds.len() as u32);
1050 self.bonds.push(BondEntry {
1051 atom1: a,
1052 atom2: b,
1053 order,
1054 });
1055 self.adjacency[a.0 as usize].push((b, bidx));
1056 self.adjacency[b.0 as usize].push((a, bidx));
1057 Ok(bidx)
1058 }
1059
1060 pub fn build(self) -> Molecule {
1062 Molecule {
1063 atoms: self.atoms,
1064 bonds: self.bonds,
1065 adjacency: self.adjacency,
1066 stereo_groups: self.stereo_groups,
1067 stereo_neighbor_order: self.stereo_neighbor_order,
1068 bond_directions: self.bond_directions,
1069 }
1070 }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075 use super::*;
1076 use crate::atom::Atom;
1077 use crate::element::Element;
1078
1079 fn ethane() -> Molecule {
1080 let mut b = MoleculeBuilder::new();
1081 let c1 = b.add_atom(Atom::new(Element::C));
1082 let c2 = b.add_atom(Atom::new(Element::C));
1083 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1084 b.build()
1085 }
1086
1087 #[test]
1088 fn test_basic_molecule() {
1089 let mol = ethane();
1090 assert_eq!(mol.atom_count(), 2);
1091 assert_eq!(mol.bond_count(), 1);
1092 }
1093
1094 #[test]
1095 fn test_adjacency() {
1096 let mol = ethane();
1097 let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1098 assert_eq!(neighbors.len(), 1);
1099 assert_eq!(neighbors[0].0, AtomIdx(1));
1100 }
1101
1102 #[test]
1103 fn test_bond_between() {
1104 let mol = ethane();
1105 assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1106 assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1107 }
1108
1109 #[test]
1110 fn test_duplicate_bond_error() {
1111 let mut b = MoleculeBuilder::new();
1112 let c1 = b.add_atom(Atom::new(Element::C));
1113 let c2 = b.add_atom(Atom::new(Element::C));
1114 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1115 let err = b.add_bond(c1, c2, BondOrder::Double);
1116 assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1117 }
1118
1119 #[test]
1120 fn test_formula() {
1121 let mut b = MoleculeBuilder::new();
1122 let c = b.add_atom(Atom::new(Element::C));
1123 let n = b.add_atom(Atom::new(Element::N));
1124 b.add_bond(c, n, BondOrder::Single).unwrap();
1125 let mol = b.build();
1126 assert_eq!(mol.formula(), "CN");
1127 }
1128
1129 #[test]
1130 fn test_implicit_hydrogen_count() {
1131 let mut b = MoleculeBuilder::new();
1133 b.add_atom(Atom::organic(Element::C));
1134 let mol = b.build();
1135 assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1136 }
1137
1138 #[test]
1139 fn test_total_formula_methane() {
1140 let mut b = MoleculeBuilder::new();
1142 b.add_atom(Atom::organic(Element::C));
1143 let mol = b.build();
1144 assert_eq!(mol.total_formula(), "CH4");
1145 }
1146
1147 #[test]
1148 fn test_total_formula_no_hydrogen() {
1149 let mut b = MoleculeBuilder::new();
1151 let na = b.add_atom(Atom::new(Element::NA));
1152 let cl = b.add_atom(Atom::new(Element::CL));
1153 b.add_bond(na, cl, BondOrder::Single).unwrap();
1154 let mol = b.build();
1155 assert_eq!(mol.total_formula(), "ClNa");
1156 }
1157
1158 #[test]
1159 fn test_with_atom_aromatic() {
1160 let mol = ethane();
1161 let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1162 assert!(updated.atom(AtomIdx(0)).aromatic);
1163 assert!(!updated.atom(AtomIdx(1)).aromatic);
1164 }
1165
1166 #[test]
1167 fn test_with_bond_order() {
1168 let mol = ethane();
1169 let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1170 assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1171 }
1172
1173 fn chain_with_direction_on_last_bond() -> (Molecule, BondIdx) {
1181 let mut b = MoleculeBuilder::new();
1182 let a = b.add_atom(Atom::new(Element::C));
1183 let bb = b.add_atom(Atom::new(Element::C));
1184 let c = b.add_atom(Atom::new(Element::C));
1185 let d = b.add_atom(Atom::new(Element::C));
1186 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);
1190 (b.build(), cd)
1191 }
1192
1193 #[test]
1194 fn test_remove_bond_remaps_bond_direction_not_misattributes() {
1195 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1196 assert_eq!(mol.bond_count(), 3);
1197 mol.remove_bond(BondIdx(0)); assert_eq!(mol.bond_count(), 2);
1199 assert_eq!(mol.bond_direction(BondIdx(1)), Some(BondOrder::Up));
1201 assert_eq!(mol.bond_direction(BondIdx(0)), None);
1205 assert_eq!(mol.bond_opt(BondIdx(2)), None);
1206 }
1207
1208 #[test]
1209 fn test_remove_bond_drops_direction_for_the_removed_bond_itself() {
1210 let (mut mol, _cd) = chain_with_direction_on_last_bond();
1211 mol.remove_bond(BondIdx(2)); assert_eq!(mol.bond_count(), 2);
1213 assert!(mol.bond_direction(BondIdx(0)).is_none());
1214 assert!(mol.bond_direction(BondIdx(1)).is_none());
1215 }
1216
1217 #[test]
1218 fn test_with_atom_removed_remaps_bond_direction() {
1219 let (mol, _cd) = chain_with_direction_on_last_bond();
1220 let (updated, _atom_remap) = mol.with_atom_removed(AtomIdx(0));
1224 assert_eq!(updated.bond_count(), 2);
1225 let has_direction = (0..updated.bond_count())
1229 .map(|i| BondIdx(i as u32))
1230 .any(|bidx| updated.bond_direction(bidx) == Some(BondOrder::Up));
1231 assert!(
1232 has_direction,
1233 "bond_direction on C-D must survive atom removal, remapped to its new bond index"
1234 );
1235 }
1236
1237 #[test]
1240 fn test_add_remove_atom() {
1241 let mut mol = ethane();
1242 let n_idx = mol.add_atom(Atom::new(Element::N));
1243 assert_eq!(mol.atom_count(), 3);
1244 assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1245
1246 let remap = mol.remove_atom(n_idx);
1247 assert_eq!(mol.atom_count(), 2);
1248 assert!(remap[n_idx.0 as usize].is_none());
1249 }
1250
1251 #[test]
1252 fn test_add_remove_bond() {
1253 let mut mol = ethane();
1254 let n_idx = mol.add_atom(Atom::new(Element::N));
1255 let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1256 assert_eq!(mol.bond_count(), 2);
1257 mol.remove_bond(bidx);
1258 assert_eq!(mol.bond_count(), 1);
1259 }
1260
1261 #[test]
1262 fn test_set_charge_element() {
1263 let mut mol = ethane();
1264 mol.set_charge(AtomIdx(0), 1);
1265 assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1266 mol.set_element(AtomIdx(0), Element::N);
1267 assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1268 }
1269
1270 #[test]
1271 fn test_is_connected() {
1272 let mol = ethane();
1273 assert!(mol.is_connected());
1274
1275 let mut b = MoleculeBuilder::new();
1277 b.add_atom(Atom::new(Element::C));
1278 b.add_atom(Atom::new(Element::N));
1279 let disconnected = b.build();
1280 assert!(!disconnected.is_connected());
1281 }
1282
1283 #[test]
1284 fn test_fragments() {
1285 let mut b = MoleculeBuilder::new();
1287 let c1 = b.add_atom(Atom::organic(Element::C));
1288 let c2 = b.add_atom(Atom::organic(Element::C));
1289 b.add_bond(c1, c2, BondOrder::Single).unwrap();
1290 b.add_atom(Atom::new(Element::N)); let mol = b.build();
1292 let frags = mol.fragments();
1293 assert_eq!(frags.len(), 2);
1294 let sizes: std::collections::HashSet<usize> =
1295 frags.iter().map(|f| f.atom_count()).collect();
1296 assert!(sizes.contains(&2));
1297 assert!(sizes.contains(&1));
1298 }
1299
1300 #[test]
1301 fn test_builder_from_molecule() {
1302 let mol = ethane();
1303 let mut b = MoleculeBuilder::from_molecule(&mol);
1304 b.add_atom(Atom::new(Element::O));
1305 let mol2 = b.build();
1306 assert_eq!(mol2.atom_count(), 3);
1307 assert_eq!(mol2.bond_count(), 1); }
1309
1310 #[test]
1313 fn test_atom_opt_valid() {
1314 let mol = ethane();
1315 assert!(mol.atom_opt(AtomIdx(0)).is_some());
1316 assert!(mol.atom_opt(AtomIdx(1)).is_some());
1317 let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1318 assert_eq!(atom.element.atomic_number(), 6);
1319 }
1320
1321 #[test]
1322 fn test_atom_opt_invalid() {
1323 let mol = ethane();
1324 assert!(mol.atom_opt(AtomIdx(2)).is_none());
1325 assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1326 }
1327
1328 #[test]
1329 fn test_bond_opt_valid() {
1330 let mol = ethane();
1331 assert!(mol.bond_opt(BondIdx(0)).is_some());
1332 let bond = mol.bond_opt(BondIdx(0)).unwrap();
1333 assert_eq!(bond.order, BondOrder::Single);
1334 }
1335
1336 #[test]
1337 fn test_bond_opt_invalid() {
1338 let mol = ethane();
1339 assert!(mol.bond_opt(BondIdx(1)).is_none());
1340 assert!(mol.bond_opt(BondIdx(1000)).is_none());
1341 }
1342
1343 #[test]
1344 fn test_neighbors_opt_valid() {
1345 let mol = ethane();
1346 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1347 assert_eq!(neighbors.len(), 1);
1348 assert_eq!(neighbors[0].0, AtomIdx(1));
1349 }
1350
1351 #[test]
1352 fn test_neighbors_opt_isolated_atom() {
1353 let mut b = MoleculeBuilder::new();
1354 b.add_atom(Atom::new(Element::C));
1355 b.add_atom(Atom::new(Element::N));
1356 let mol = b.build();
1357 let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1358 assert_eq!(neighbors.len(), 0);
1359 }
1360
1361 #[test]
1362 fn test_neighbors_opt_invalid() {
1363 let mol = ethane();
1364 assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1365 assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1366 }
1367
1368 #[test]
1369 fn test_degree_opt_valid() {
1370 let mol = ethane();
1371 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1372 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1373 }
1374
1375 #[test]
1376 fn test_degree_opt_isolated_atom() {
1377 let mut b = MoleculeBuilder::new();
1378 b.add_atom(Atom::new(Element::C));
1379 b.add_atom(Atom::new(Element::N));
1380 let mol = b.build();
1381 assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1382 assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1383 }
1384
1385 #[test]
1386 fn test_degree_opt_invalid() {
1387 let mol = ethane();
1388 assert!(mol.degree_opt(AtomIdx(2)).is_none());
1389 assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1390 }
1391
1392 #[test]
1393 fn test_degree_opt_multiple_bonds() {
1394 let mut b = MoleculeBuilder::new();
1396 let center = b.add_atom(Atom::new(Element::C));
1397 let n1 = b.add_atom(Atom::new(Element::C));
1398 let n2 = b.add_atom(Atom::new(Element::N));
1399 let n3 = b.add_atom(Atom::new(Element::O));
1400 b.add_bond(center, n1, BondOrder::Single).unwrap();
1401 b.add_bond(center, n2, BondOrder::Double).unwrap();
1402 b.add_bond(center, n3, BondOrder::Single).unwrap();
1403 let mol = b.build();
1404 assert_eq!(mol.degree_opt(center), Some(3));
1405 assert_eq!(mol.degree_opt(n1), Some(1));
1406 assert_eq!(mol.degree_opt(n2), Some(1));
1407 assert_eq!(mol.degree_opt(n3), Some(1));
1408 }
1409}