Skip to main content

chematic_core/
molecule.rs

1//! Molecule graph: atoms, bonds, and adjacency list.
2
3use crate::atom::Atom;
4use crate::bond::{BondEntry, BondOrder};
5use crate::element::Element;
6use crate::stereo_group::StereoGroup;
7
8/// Newtype index for an atom in a Molecule.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct AtomIdx(pub u32);
11
12/// Newtype index for a bond in a Molecule.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct BondIdx(pub u32);
15
16/// Error types for molecule construction.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum MolError {
19    /// Atom index out of range.
20    InvalidAtomIdx(AtomIdx),
21    /// Duplicate bond between the same pair of atoms.
22    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
38/// An immutable molecular graph built via [`MoleculeBuilder`].
39///
40/// Representation: atom list + bond list + per-atom adjacency list.
41/// No external graph library is used; all graph traversal is domain-aware.
42/// Sentinel used in `stereo_neighbor_order` to represent the implicit H in a bracket atom.
43pub const STEREO_H_SENTINEL: u32 = u32::MAX;
44
45#[derive(Clone)]
46pub struct Molecule {
47    atoms: Vec<Atom>,
48    bonds: Vec<BondEntry>,
49    /// adjacency[atom_idx] = list of (neighbor_atom_idx, bond_idx)
50    adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
51    /// Enhanced stereo groups (ChemDraw V3000 Absolute / Or / And).
52    stereo_groups: Vec<StereoGroup>,
53    /// SMILES-text-order neighbor sequence for chiral atoms.
54    ///
55    /// Keyed by atom index.  Each value lists the atom indices of neighbors in
56    /// the order they appeared in the SMILES string (including ring-closure
57    /// partners), with [`STEREO_H_SENTINEL`] (`u32::MAX`) standing in for the
58    /// implicit bracket H.  Populated by the SMILES parser; absent for atoms
59    /// not parsed from SMILES or without recorded stereo.
60    stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
61    /// Directional (`/`, `\`) bond marker, stashed for bonds whose `order` was
62    /// overwritten to `Aromatic` (e.g. an exocyclic C=N adjacent to an
63    /// aromatic ring atom — `order` must stay `Aromatic` for SMARTS `:a`
64    /// matching, but the E/Z direction would otherwise be lost). Keyed by
65    /// bond index; value is `BondOrder::Up` or `BondOrder::Down`. Absent for
66    /// bonds whose direction is already carried directly by `order`.
67    bond_directions: std::collections::HashMap<u32, BondOrder>,
68}
69
70impl Molecule {
71    /// Number of heavy atoms (does not count implicit H).
72    pub fn atom_count(&self) -> usize {
73        self.atoms.len()
74    }
75
76    /// Number of bonds (edges).
77    pub fn bond_count(&self) -> usize {
78        self.bonds.len()
79    }
80
81    /// Borrow atom by index.
82    ///
83    /// # Panics
84    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
85    ///
86    /// For a non-panicking variant, use [`Self::atom_opt`].
87    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    /// Borrow atom by index, returning `None` if out of range.
100    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    /// Borrow bond by index.
110    ///
111    /// # Panics
112    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
113    ///
114    /// For a non-panicking variant, use [`Self::bond_opt`].
115    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    /// Borrow bond by index, returning `None` if out of range.
128    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    /// Iterate over all atoms as `(AtomIdx, &Atom)`.
138    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    /// Iterate over all bonds as `(BondIdx, &BondEntry)`.
146    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    /// Iterate over neighbors of `idx` as `(neighbor_atom_idx, bond_idx)`.
154    ///
155    /// # Panics
156    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
157    ///
158    /// For a non-panicking variant, use [`Self::neighbors_opt`].
159    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    /// Iterate over neighbors of `idx` as `(neighbor_atom_idx, bond_idx)`, returning `None` if out of range.
172    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    /// Degree (number of connected bonds) of atom `idx`.
182    ///
183    /// # Panics
184    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
185    ///
186    /// For a non-panicking variant, use [`Self::degree_opt`].
187    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    /// Degree (number of connected bonds) of atom `idx`, returning `None` if out of range.
200    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    /// Return the bond between `a` and `b`, or `None` if not connected or indices are out of bounds.
210    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    /// Molecular formula as a Hill-order string (C first, H second, then alphabetical).
230    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
249// ---------------------------------------------------------------------------
250// Immutable update methods (functional-style editing)
251// ---------------------------------------------------------------------------
252
253impl Molecule {
254    /// Format element counts in Hill order: C, H, then alphabetically.
255    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    /// Return a new `Molecule` with one extra atom appended, along with the
279    /// index that the new atom will have in the returned molecule.
280    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    /// Return a new `Molecule` with one extra bond added, along with the index
287    /// of the newly added bond in the returned molecule.
288    ///
289    /// Returns `Err` if `a == b` or the bond already exists (same semantics as
290    /// [`MoleculeBuilder::add_bond`]).
291    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    /// Return a new `Molecule` with the formal charge of atom `idx` changed.
303    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    /// Return a new `Molecule` with the element of atom `idx` changed.
321    ///
322    /// Chirality and hydrogen count are reset to `None` when the element
323    /// changes, since those properties are element-specific.
324    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                // Reset element-specific fields so valence stays consistent.
331                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        // Chirality was cleared for the changed atom; remove its stereo order too.
343        builder.clear_stereo_neighbor_order(idx);
344        builder.build()
345    }
346
347    /// Return a new `Molecule` with atom `idx` and all bonds involving it
348    /// removed.  Atom indices of survivors shift down past the removed slot.
349    ///
350    /// The returned tuple also includes a mapping from **old** `AtomIdx` to
351    /// **new** `AtomIdx` (indices that fall below `idx` are unchanged; indices
352    /// above `idx` decrease by 1).
353    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        // Build old→new index table.
358        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        for (_, bond) in self.bonds() {
376            if bond.atom1 == idx || bond.atom2 == idx {
377                continue;
378            }
379            if let (Some(a1), Some(a2)) =
380                (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
381            {
382                let _ = builder.add_bond(a1, a2, bond.order);
383            }
384        }
385        // Remap stereo neighbor order: drop removed atom's entry, remap neighbor indices.
386        for (old_key, order) in &self.stereo_neighbor_order {
387            let old_atom = *old_key as usize;
388            if old_atom == removed {
389                continue; // removed atom's stereo is gone
390            }
391            if let Some(Some(new_key)) = remap.get(old_atom) {
392                let new_order: Vec<u32> = order
393                    .iter()
394                    .filter_map(|&v| {
395                        if v == STEREO_H_SENTINEL {
396                            Some(STEREO_H_SENTINEL)
397                        } else if v as usize == removed {
398                            None // neighbor was the removed atom — stereo is now invalid
399                        } else {
400                            remap.get(v as usize).and_then(|r| r.map(|a| a.0))
401                        }
402                    })
403                    .collect();
404                builder.set_stereo_neighbor_order(*new_key, new_order);
405            }
406        }
407        (builder.build(), remap)
408    }
409
410    /// Implicit hydrogen count for atom `idx` based on valence rules.
411    ///
412    /// Delegates to [`crate::valence::implicit_hcount`].
413    pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
414        crate::valence::implicit_hcount(self, idx)
415    }
416
417    /// Hill-order molecular formula including implicit hydrogens.
418    ///
419    /// Unlike [`Self::formula`] (which counts only explicit heavy atoms),
420    /// this method adds the implicit H count for every atom so the result
421    /// reflects the true molecular composition (e.g. methane → "CH4").
422    pub fn total_formula(&self) -> String {
423        use std::collections::BTreeMap;
424        let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
425        let mut implicit_h: u32 = 0;
426        for (aidx, atom) in self.atoms() {
427            *counts.entry(atom.element.symbol()).or_insert(0) += 1;
428            implicit_h += crate::valence::implicit_hcount(self, aidx) as u32;
429        }
430        *counts.entry("H").or_insert(0) += implicit_h;
431        Self::format_hill_order_formula(&counts)
432    }
433
434    /// Hill-order molecular formula with isotope labels.
435    ///
436    /// Like [`Self::formula`] but prefixes each element symbol with its
437    /// isotope number when `atom.isotope` is `Some(n)`.
438    /// Example: a molecule with one `¹³C` and one `O` → `"¹³CO"`.
439    pub fn formula_with_isotopes(&self) -> String {
440        use std::collections::BTreeMap;
441        // Collect (isotope_prefix + symbol) counts, heavy atoms only.
442        let mut counts: BTreeMap<String, u32> = BTreeMap::new();
443        let mut has_carbon = false;
444        let mut has_explicit_h = false;
445        for (_, atom) in self.atoms() {
446            let sym = atom.element.symbol();
447            let key = match atom.isotope {
448                Some(n) => format!("{n}{sym}"),
449                None => sym.to_string(),
450            };
451            if sym == "C" && atom.isotope.is_none() {
452                has_carbon = true;
453            }
454            if sym == "H" {
455                has_explicit_h = true;
456            }
457            *counts.entry(key).or_insert(0) += 1;
458        }
459
460        let push_count = |key: &str, n: u32, out: &mut String| {
461            out.push_str(key);
462            if n > 1 {
463                out.push_str(&n.to_string());
464            }
465        };
466
467        let mut result = String::new();
468        // Hill order: C first (if unlabelled C present), then H, then rest alphabetically.
469        if has_carbon && let Some(c) = counts.remove("C") {
470            push_count("C", c, &mut result);
471        }
472        if has_explicit_h && let Some(h) = counts.remove("H") {
473            push_count("H", h, &mut result);
474        }
475        for (key, count) in &counts {
476            push_count(key, *count, &mut result);
477        }
478        result
479    }
480
481    /// Return a new `Molecule` with atom `idx`'s aromatic flag changed.
482    pub fn with_atom_aromatic(&self, idx: AtomIdx, aromatic: bool) -> Molecule {
483        let mut builder = MoleculeBuilder::new();
484        for (aidx, atom) in self.atoms() {
485            let mut a = atom.clone();
486            if aidx == idx {
487                a.aromatic = aromatic;
488            }
489            builder.add_atom(a);
490        }
491        for (_, bond) in self.bonds() {
492            let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
493        }
494        builder.copy_stereo_from(self);
495        builder.copy_bond_directions_from(self);
496        builder.build()
497    }
498
499    /// Return a new `Molecule` with bond `idx`'s order changed.
500    pub fn with_bond_order(&self, idx: BondIdx, order: BondOrder) -> Molecule {
501        let mut builder = MoleculeBuilder::new();
502        for (_, atom) in self.atoms() {
503            builder.add_atom(atom.clone());
504        }
505        for (bidx, bond) in self.bonds() {
506            let o = if bidx == idx { order } else { bond.order };
507            let _ = builder.add_bond(bond.atom1, bond.atom2, o);
508        }
509        builder.copy_stereo_from(self);
510        builder.copy_bond_directions_from(self);
511        builder.build()
512    }
513
514    /// Return a new `Molecule` with bond `idx` removed.
515    ///
516    /// Atom indices are unchanged.  Bond indices of survivors shift down, so
517    /// `bond_directions` (keyed by bond index) is remapped bond-by-bond
518    /// rather than copied wholesale — `copy_bond_directions_from` would
519    /// misattribute directions to the wrong bond for every survivor after
520    /// the removed one.
521    pub fn with_bond_removed(&self, idx: BondIdx) -> Molecule {
522        let mut builder = MoleculeBuilder::new();
523        for (_, atom) in self.atoms() {
524            builder.add_atom(atom.clone());
525        }
526        for (bidx, bond) in self.bonds() {
527            if bidx == idx {
528                continue;
529            }
530            if let Ok(new_bidx) = builder.add_bond(bond.atom1, bond.atom2, bond.order)
531                && let Some(direction) = self.bond_direction(bidx)
532            {
533                builder.set_bond_direction(new_bidx, direction);
534            }
535        }
536        builder.copy_stereo_from(self);
537        builder.build()
538    }
539}
540
541// ---------------------------------------------------------------------------
542// In-place mutation methods
543// ---------------------------------------------------------------------------
544
545impl Molecule {
546    /// Append a new atom and return its index.
547    pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
548        let idx = AtomIdx(self.atoms.len() as u32);
549        self.atoms.push(atom);
550        self.adjacency.push(vec![]);
551        idx
552    }
553
554    /// Remove atom `idx` and all bonds involving it.
555    ///
556    /// Returns a remapping table: `remap[old_idx]` gives the new `AtomIdx`
557    /// for surviving atoms, or `None` for the removed atom.  Atom indices
558    /// of atoms after the removed slot shift down by 1.
559    pub fn remove_atom(&mut self, idx: AtomIdx) -> Vec<Option<AtomIdx>> {
560        let n = self.atoms.len();
561        let removed = idx.0 as usize;
562
563        let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
564        let mut new_pos = 0u32;
565        for (old, slot) in remap.iter_mut().enumerate() {
566            if old == removed {
567                continue;
568            }
569            *slot = Some(AtomIdx(new_pos));
570            new_pos += 1;
571        }
572
573        self.atoms.remove(removed);
574
575        // Keep only bonds not involving the removed atom; remap endpoints and
576        // track each surviving bond's new index so `bond_directions` (keyed
577        // by bond index) can be remapped the same way as `stereo_neighbor_order`
578        // is remapped by atom index below.
579        let mut new_bonds: Vec<BondEntry> = Vec::new();
580        let mut bond_remap: Vec<Option<u32>> = vec![None; self.bonds.len()];
581        for (old_bidx, bond) in self.bonds.iter().enumerate() {
582            if bond.atom1 == idx || bond.atom2 == idx {
583                continue;
584            }
585            if let (Some(a1), Some(a2)) =
586                (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
587            {
588                bond_remap[old_bidx] = Some(new_bonds.len() as u32);
589                new_bonds.push(BondEntry {
590                    atom1: a1,
591                    atom2: a2,
592                    order: bond.order,
593                });
594            }
595        }
596        self.bonds = new_bonds;
597
598        // Remap bond directions in-place, same shift as bond_remap above.
599        let old_bond_directions = std::mem::take(&mut self.bond_directions);
600        for (old_key, direction) in old_bond_directions {
601            if let Some(Some(new_key)) = bond_remap.get(old_key as usize) {
602                self.bond_directions.insert(*new_key, direction);
603            }
604        }
605
606        // Rebuild adjacency from scratch.
607        let new_n = self.atoms.len();
608        self.adjacency = vec![vec![]; new_n];
609        for (bidx, bond) in self.bonds.iter().enumerate() {
610            let bi = BondIdx(bidx as u32);
611            self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
612            self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
613        }
614
615        // Remap stereo neighbor order in-place.
616        let old_stereo = std::mem::take(&mut self.stereo_neighbor_order);
617        for (old_key, order) in old_stereo {
618            let old_atom = old_key as usize;
619            if old_atom == removed {
620                continue;
621            }
622            if let Some(Some(new_key)) = remap.get(old_atom) {
623                let new_order: Vec<u32> = order
624                    .iter()
625                    .filter_map(|&v| {
626                        if v == STEREO_H_SENTINEL {
627                            Some(STEREO_H_SENTINEL)
628                        } else if v as usize == removed {
629                            None
630                        } else {
631                            remap.get(v as usize).and_then(|r| r.map(|a| a.0))
632                        }
633                    })
634                    .collect();
635                self.stereo_neighbor_order.insert(new_key.0, new_order);
636            }
637        }
638
639        remap
640    }
641
642    /// Add a bond between `a` and `b` with the given `order`.
643    ///
644    /// Returns `Err` if `a == b` or the bond already exists.
645    pub fn add_bond(
646        &mut self,
647        a: AtomIdx,
648        b: AtomIdx,
649        order: BondOrder,
650    ) -> Result<BondIdx, MolError> {
651        let n = self.atoms.len() as u32;
652        if a.0 >= n {
653            return Err(MolError::InvalidAtomIdx(a));
654        }
655        if b.0 >= n {
656            return Err(MolError::InvalidAtomIdx(b));
657        }
658        if self.adjacency[a.0 as usize].iter().any(|&(nb, _)| nb == b) {
659            return Err(MolError::DuplicateBond(a, b));
660        }
661        let bidx = BondIdx(self.bonds.len() as u32);
662        self.bonds.push(BondEntry {
663            atom1: a,
664            atom2: b,
665            order,
666        });
667        self.adjacency[a.0 as usize].push((b, bidx));
668        self.adjacency[b.0 as usize].push((a, bidx));
669        Ok(bidx)
670    }
671
672    /// Remove bond `idx`.  Atom indices are unchanged; bond indices of
673    /// surviving bonds shift down past the removed slot.
674    pub fn remove_bond(&mut self, idx: BondIdx) {
675        let removed = idx.0 as usize;
676        if removed >= self.bonds.len() {
677            return;
678        }
679        self.bonds.remove(removed);
680        // Rebuild adjacency with renumbered bond indices.
681        let n = self.atoms.len();
682        self.adjacency = vec![vec![]; n];
683        for (bidx, bond) in self.bonds.iter().enumerate() {
684            let bi = BondIdx(bidx as u32);
685            self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
686            self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
687        }
688    }
689
690    /// Set the formal charge of atom `idx` in-place.
691    pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
692        self.atoms[idx.0 as usize].charge = charge;
693    }
694
695    /// Set the element of atom `idx` in-place.
696    ///
697    /// Chirality and hydrogen count are reset (element-specific properties).
698    pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
699        let a = &mut self.atoms[idx.0 as usize];
700        a.element = el;
701        a.chirality = crate::atom::Chirality::None;
702        a.hydrogen_count = None;
703        a.aromatic = false;
704    }
705
706    /// Set the CIP stereo code of atom `idx` in-place.
707    pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
708        self.atoms[idx.0 as usize].cip_code = code;
709    }
710
711    /// Set the tetrahedral chirality (`@`/`@@`) of atom `idx` in-place.
712    pub fn set_chirality(&mut self, idx: AtomIdx, chirality: crate::atom::Chirality) {
713        self.atoms[idx.0 as usize].chirality = chirality;
714    }
715
716    /// Return the enhanced stereo groups attached to this molecule.
717    pub fn stereo_groups(&self) -> &[StereoGroup] {
718        &self.stereo_groups
719    }
720
721    /// Replace the stereo group list in-place.
722    pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
723        self.stereo_groups = groups;
724    }
725
726    /// Add a single stereo group in-place.
727    pub fn add_stereo_group(&mut self, group: StereoGroup) {
728        self.stereo_groups.push(group);
729    }
730
731    /// SMILES-text-order neighbor sequence for a chiral atom.
732    ///
733    /// Returns `None` for atoms not parsed from SMILES or without stereo.
734    /// The slice contains neighbor atom indices in SMILES text order;
735    /// [`STEREO_H_SENTINEL`] (`u32::MAX`) marks the implicit bracket-H slot.
736    pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
737        self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
738    }
739
740    /// Set the SMILES stereo neighbor order for atom `idx`.
741    pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
742        self.stereo_neighbor_order.insert(idx.0, order);
743    }
744
745    /// Directional (`/`, `\`) marker stashed for bond `idx`, if its `order`
746    /// was overwritten to `Aromatic` while it still carried E/Z direction.
747    /// Returns `BondOrder::Up` or `BondOrder::Down` when present.
748    pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
749        self.bond_directions.get(&idx.0).copied()
750    }
751
752    /// Stash a directional marker for bond `idx` (see [`Self::bond_direction`]).
753    pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
754        self.bond_directions.insert(idx.0, direction);
755    }
756}
757
758// ---------------------------------------------------------------------------
759// Connectivity utilities
760// ---------------------------------------------------------------------------
761
762impl Molecule {
763    /// Return `true` if the molecule has exactly one connected component
764    /// (i.e. every atom can be reached from every other atom).
765    pub fn is_connected(&self) -> bool {
766        let n = self.atoms.len();
767        if n == 0 {
768            return true;
769        }
770        let mut visited = vec![false; n];
771        let mut stack = vec![AtomIdx(0)];
772        visited[0] = true;
773        let mut count = 1;
774        while let Some(cur) = stack.pop() {
775            for (nb, _) in self.neighbors(cur) {
776                if !visited[nb.0 as usize] {
777                    visited[nb.0 as usize] = true;
778                    count += 1;
779                    stack.push(nb);
780                }
781            }
782        }
783        count == n
784    }
785
786    /// Split the molecule into its connected components.
787    ///
788    /// Returns a `Vec` of sub-molecules, one per component.  Atoms are
789    /// renumbered within each sub-molecule starting at index 0.
790    pub fn fragments(&self) -> Vec<Molecule> {
791        let n = self.atoms.len();
792        if n == 0 {
793            return vec![];
794        }
795
796        let mut component: Vec<usize> = vec![usize::MAX; n];
797        let mut comp_id = 0;
798
799        for start in 0..n {
800            if component[start] != usize::MAX {
801                continue;
802            }
803            let mut stack = vec![start];
804            component[start] = comp_id;
805            while let Some(cur) = stack.pop() {
806                for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
807                    let ni = nb.0 as usize;
808                    if component[ni] == usize::MAX {
809                        component[ni] = comp_id;
810                        stack.push(ni);
811                    }
812                }
813            }
814            comp_id += 1;
815        }
816
817        (0..comp_id)
818            .map(|cid| {
819                let mut builder = MoleculeBuilder::new();
820                let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
821                    std::collections::HashMap::new();
822                for (aidx, atom) in self.atoms() {
823                    if component[aidx.0 as usize] == cid {
824                        let new_idx = builder.add_atom(atom.clone());
825                        old_to_new.insert(aidx, new_idx);
826                    }
827                }
828                for (_, bond) in self.bonds() {
829                    if let (Some(&a1), Some(&a2)) =
830                        (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
831                    {
832                        let _ = builder.add_bond(a1, a2, bond.order);
833                    }
834                }
835                builder.build()
836            })
837            .collect()
838    }
839}
840
841/// Builder for constructing a [`Molecule`] incrementally.
842///
843/// Usage: add atoms, add bonds, then call `build()`.
844#[derive(Default)]
845pub struct MoleculeBuilder {
846    atoms: Vec<Atom>,
847    bonds: Vec<BondEntry>,
848    adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
849    stereo_groups: Vec<StereoGroup>,
850    stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
851    bond_directions: std::collections::HashMap<u32, BondOrder>,
852}
853
854impl MoleculeBuilder {
855    pub fn new() -> Self {
856        Self::default()
857    }
858
859    /// Create a builder pre-populated with all atoms and bonds from `mol`.
860    ///
861    /// Use this to make incremental edits to an existing molecule instead of
862    /// reconstructing it from scratch.
863    pub fn from_molecule(mol: &Molecule) -> Self {
864        let mut b = Self::new();
865        for (_, atom) in mol.atoms() {
866            b.add_atom(atom.clone());
867        }
868        for (_, bond) in mol.bonds() {
869            let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
870        }
871        b.stereo_groups = mol.stereo_groups.clone();
872        b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
873        b.bond_directions = mol.bond_directions.clone();
874        b
875    }
876
877    /// Set the SMILES stereo neighbor order for atom `idx`.
878    pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
879        self.stereo_neighbor_order.insert(idx.0, order);
880    }
881
882    /// Remove the stereo neighbor order entry for atom `idx`.
883    pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
884        self.stereo_neighbor_order.remove(&idx.0);
885    }
886
887    /// Append a stereo group to this builder.
888    pub fn add_stereo_group(&mut self, group: StereoGroup) {
889        self.stereo_groups.push(group);
890    }
891
892    /// Copy all enhanced stereo groups from `mol` into this builder verbatim.
893    ///
894    /// Only valid when atom indices are unchanged from `mol` (atoms re-added
895    /// in the same order, none removed) — same caveat as
896    /// [`Self::copy_bond_directions_from`].
897    pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
898        self.stereo_groups = mol.stereo_groups.clone();
899    }
900
901    /// Copy all stereo neighbor order entries from `mol` into this builder.
902    pub fn copy_stereo_from(&mut self, mol: &Molecule) {
903        self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
904    }
905
906    /// Stash a directional marker for bond `idx` (see [`Molecule::bond_direction`]).
907    pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
908        self.bond_directions.insert(idx.0, direction);
909    }
910
911    /// Copy all bond-direction entries from `mol` into this builder verbatim.
912    ///
913    /// Only valid when bond indices are unchanged from `mol` (atoms/bonds
914    /// re-added in the same order, none skipped) — e.g. a rebuild that only
915    /// touches atom fields or promotes bond order to `Aromatic`. A rebuild
916    /// that removes or reorders bonds must remap directions bond-by-bond
917    /// instead (see `Molecule::with_bond_removed`).
918    pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
919        self.bond_directions = mol.bond_directions.clone();
920    }
921
922    /// Read-only reference to an atom already added to the builder.
923    ///
924    /// Used by the SMILES parser to infer implicit bond types without
925    /// consuming the builder (e.g. aromatic-aromatic → Aromatic bond).
926    ///
927    /// # Panics
928    /// Panics if `idx` is out of range.
929    pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
930        &self.atoms[idx.0 as usize]
931    }
932
933    /// Number of atoms added so far.
934    pub fn atom_count(&self) -> usize {
935        self.atoms.len()
936    }
937
938    /// Iterate over already-added neighbors of `idx` as `(bond_idx, neighbor_atom_idx)`.
939    /// Used by kekulization tests to check whether a bond already exists in the builder.
940    pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
941        self.adjacency[idx.0 as usize]
942            .iter()
943            .map(|&(nb, bidx)| (bidx, nb))
944    }
945
946    /// Add an atom and return its index.
947    pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
948        let idx = AtomIdx(self.atoms.len() as u32);
949        self.atoms.push(atom);
950        self.adjacency.push(Vec::new());
951        idx
952    }
953
954    /// Add a bond between two existing atoms.
955    ///
956    /// Returns an error if either atom index is invalid or if the bond already exists.
957    pub fn add_bond(
958        &mut self,
959        a: AtomIdx,
960        b: AtomIdx,
961        order: BondOrder,
962    ) -> Result<BondIdx, MolError> {
963        let n = self.atoms.len() as u32;
964        if a.0 >= n {
965            return Err(MolError::InvalidAtomIdx(a));
966        }
967        if b.0 >= n {
968            return Err(MolError::InvalidAtomIdx(b));
969        }
970
971        // Check for duplicate
972        for &(nb, _) in &self.adjacency[a.0 as usize] {
973            if nb == b {
974                return Err(MolError::DuplicateBond(a, b));
975            }
976        }
977
978        let bidx = BondIdx(self.bonds.len() as u32);
979        self.bonds.push(BondEntry {
980            atom1: a,
981            atom2: b,
982            order,
983        });
984        self.adjacency[a.0 as usize].push((b, bidx));
985        self.adjacency[b.0 as usize].push((a, bidx));
986        Ok(bidx)
987    }
988
989    /// Consume the builder and return an immutable [`Molecule`].
990    pub fn build(self) -> Molecule {
991        Molecule {
992            atoms: self.atoms,
993            bonds: self.bonds,
994            adjacency: self.adjacency,
995            stereo_groups: self.stereo_groups,
996            stereo_neighbor_order: self.stereo_neighbor_order,
997            bond_directions: self.bond_directions,
998        }
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005    use crate::atom::Atom;
1006    use crate::element::Element;
1007
1008    fn ethane() -> Molecule {
1009        let mut b = MoleculeBuilder::new();
1010        let c1 = b.add_atom(Atom::new(Element::C));
1011        let c2 = b.add_atom(Atom::new(Element::C));
1012        b.add_bond(c1, c2, BondOrder::Single).unwrap();
1013        b.build()
1014    }
1015
1016    #[test]
1017    fn test_basic_molecule() {
1018        let mol = ethane();
1019        assert_eq!(mol.atom_count(), 2);
1020        assert_eq!(mol.bond_count(), 1);
1021    }
1022
1023    #[test]
1024    fn test_adjacency() {
1025        let mol = ethane();
1026        let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1027        assert_eq!(neighbors.len(), 1);
1028        assert_eq!(neighbors[0].0, AtomIdx(1));
1029    }
1030
1031    #[test]
1032    fn test_bond_between() {
1033        let mol = ethane();
1034        assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1035        assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1036    }
1037
1038    #[test]
1039    fn test_duplicate_bond_error() {
1040        let mut b = MoleculeBuilder::new();
1041        let c1 = b.add_atom(Atom::new(Element::C));
1042        let c2 = b.add_atom(Atom::new(Element::C));
1043        b.add_bond(c1, c2, BondOrder::Single).unwrap();
1044        let err = b.add_bond(c1, c2, BondOrder::Double);
1045        assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1046    }
1047
1048    #[test]
1049    fn test_formula() {
1050        let mut b = MoleculeBuilder::new();
1051        let c = b.add_atom(Atom::new(Element::C));
1052        let n = b.add_atom(Atom::new(Element::N));
1053        b.add_bond(c, n, BondOrder::Single).unwrap();
1054        let mol = b.build();
1055        assert_eq!(mol.formula(), "CN");
1056    }
1057
1058    #[test]
1059    fn test_implicit_hydrogen_count() {
1060        // Isolated C atom (sp3, 4 bonds available): 4 implicit H
1061        let mut b = MoleculeBuilder::new();
1062        b.add_atom(Atom::organic(Element::C));
1063        let mol = b.build();
1064        assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1065    }
1066
1067    #[test]
1068    fn test_total_formula_methane() {
1069        // Organic C atom with 0 explicit bonds → 4 implicit H → CH4
1070        let mut b = MoleculeBuilder::new();
1071        b.add_atom(Atom::organic(Element::C));
1072        let mol = b.build();
1073        assert_eq!(mol.total_formula(), "CH4");
1074    }
1075
1076    #[test]
1077    fn test_total_formula_no_hydrogen() {
1078        // NaCl — neither Na nor Cl is in the organic subset, no implicit H
1079        let mut b = MoleculeBuilder::new();
1080        let na = b.add_atom(Atom::new(Element::NA));
1081        let cl = b.add_atom(Atom::new(Element::CL));
1082        b.add_bond(na, cl, BondOrder::Single).unwrap();
1083        let mol = b.build();
1084        assert_eq!(mol.total_formula(), "ClNa");
1085    }
1086
1087    #[test]
1088    fn test_with_atom_aromatic() {
1089        let mol = ethane();
1090        let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1091        assert!(updated.atom(AtomIdx(0)).aromatic);
1092        assert!(!updated.atom(AtomIdx(1)).aromatic);
1093    }
1094
1095    #[test]
1096    fn test_with_bond_order() {
1097        let mol = ethane();
1098        let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1099        assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1100    }
1101
1102    // --- mutable API ---
1103
1104    #[test]
1105    fn test_add_remove_atom() {
1106        let mut mol = ethane();
1107        let n_idx = mol.add_atom(Atom::new(Element::N));
1108        assert_eq!(mol.atom_count(), 3);
1109        assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1110
1111        let remap = mol.remove_atom(n_idx);
1112        assert_eq!(mol.atom_count(), 2);
1113        assert!(remap[n_idx.0 as usize].is_none());
1114    }
1115
1116    #[test]
1117    fn test_add_remove_bond() {
1118        let mut mol = ethane();
1119        let n_idx = mol.add_atom(Atom::new(Element::N));
1120        let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1121        assert_eq!(mol.bond_count(), 2);
1122        mol.remove_bond(bidx);
1123        assert_eq!(mol.bond_count(), 1);
1124    }
1125
1126    #[test]
1127    fn test_set_charge_element() {
1128        let mut mol = ethane();
1129        mol.set_charge(AtomIdx(0), 1);
1130        assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1131        mol.set_element(AtomIdx(0), Element::N);
1132        assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1133    }
1134
1135    #[test]
1136    fn test_is_connected() {
1137        let mol = ethane();
1138        assert!(mol.is_connected());
1139
1140        // Two separate atoms — disconnected
1141        let mut b = MoleculeBuilder::new();
1142        b.add_atom(Atom::new(Element::C));
1143        b.add_atom(Atom::new(Element::N));
1144        let disconnected = b.build();
1145        assert!(!disconnected.is_connected());
1146    }
1147
1148    #[test]
1149    fn test_fragments() {
1150        // "CC.N" — two components
1151        let mut b = MoleculeBuilder::new();
1152        let c1 = b.add_atom(Atom::organic(Element::C));
1153        let c2 = b.add_atom(Atom::organic(Element::C));
1154        b.add_bond(c1, c2, BondOrder::Single).unwrap();
1155        b.add_atom(Atom::new(Element::N)); // disconnected N
1156        let mol = b.build();
1157        let frags = mol.fragments();
1158        assert_eq!(frags.len(), 2);
1159        let sizes: std::collections::HashSet<usize> =
1160            frags.iter().map(|f| f.atom_count()).collect();
1161        assert!(sizes.contains(&2));
1162        assert!(sizes.contains(&1));
1163    }
1164
1165    #[test]
1166    fn test_builder_from_molecule() {
1167        let mol = ethane();
1168        let mut b = MoleculeBuilder::from_molecule(&mol);
1169        b.add_atom(Atom::new(Element::O));
1170        let mol2 = b.build();
1171        assert_eq!(mol2.atom_count(), 3);
1172        assert_eq!(mol2.bond_count(), 1); // original bond preserved
1173    }
1174
1175    // --- safe Option-returning variants ---
1176
1177    #[test]
1178    fn test_atom_opt_valid() {
1179        let mol = ethane();
1180        assert!(mol.atom_opt(AtomIdx(0)).is_some());
1181        assert!(mol.atom_opt(AtomIdx(1)).is_some());
1182        let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1183        assert_eq!(atom.element.atomic_number(), 6);
1184    }
1185
1186    #[test]
1187    fn test_atom_opt_invalid() {
1188        let mol = ethane();
1189        assert!(mol.atom_opt(AtomIdx(2)).is_none());
1190        assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1191    }
1192
1193    #[test]
1194    fn test_bond_opt_valid() {
1195        let mol = ethane();
1196        assert!(mol.bond_opt(BondIdx(0)).is_some());
1197        let bond = mol.bond_opt(BondIdx(0)).unwrap();
1198        assert_eq!(bond.order, BondOrder::Single);
1199    }
1200
1201    #[test]
1202    fn test_bond_opt_invalid() {
1203        let mol = ethane();
1204        assert!(mol.bond_opt(BondIdx(1)).is_none());
1205        assert!(mol.bond_opt(BondIdx(1000)).is_none());
1206    }
1207
1208    #[test]
1209    fn test_neighbors_opt_valid() {
1210        let mol = ethane();
1211        let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1212        assert_eq!(neighbors.len(), 1);
1213        assert_eq!(neighbors[0].0, AtomIdx(1));
1214    }
1215
1216    #[test]
1217    fn test_neighbors_opt_isolated_atom() {
1218        let mut b = MoleculeBuilder::new();
1219        b.add_atom(Atom::new(Element::C));
1220        b.add_atom(Atom::new(Element::N));
1221        let mol = b.build();
1222        let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1223        assert_eq!(neighbors.len(), 0);
1224    }
1225
1226    #[test]
1227    fn test_neighbors_opt_invalid() {
1228        let mol = ethane();
1229        assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1230        assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1231    }
1232
1233    #[test]
1234    fn test_degree_opt_valid() {
1235        let mol = ethane();
1236        assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1237        assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1238    }
1239
1240    #[test]
1241    fn test_degree_opt_isolated_atom() {
1242        let mut b = MoleculeBuilder::new();
1243        b.add_atom(Atom::new(Element::C));
1244        b.add_atom(Atom::new(Element::N));
1245        let mol = b.build();
1246        assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1247        assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1248    }
1249
1250    #[test]
1251    fn test_degree_opt_invalid() {
1252        let mol = ethane();
1253        assert!(mol.degree_opt(AtomIdx(2)).is_none());
1254        assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1255    }
1256
1257    #[test]
1258    fn test_degree_opt_multiple_bonds() {
1259        // Create a central atom with 3 neighbors
1260        let mut b = MoleculeBuilder::new();
1261        let center = b.add_atom(Atom::new(Element::C));
1262        let n1 = b.add_atom(Atom::new(Element::C));
1263        let n2 = b.add_atom(Atom::new(Element::N));
1264        let n3 = b.add_atom(Atom::new(Element::O));
1265        b.add_bond(center, n1, BondOrder::Single).unwrap();
1266        b.add_bond(center, n2, BondOrder::Double).unwrap();
1267        b.add_bond(center, n3, BondOrder::Single).unwrap();
1268        let mol = b.build();
1269        assert_eq!(mol.degree_opt(center), Some(3));
1270        assert_eq!(mol.degree_opt(n1), Some(1));
1271        assert_eq!(mol.degree_opt(n2), Some(1));
1272        assert_eq!(mol.degree_opt(n3), Some(1));
1273    }
1274}