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
45pub struct Molecule {
46    atoms: Vec<Atom>,
47    bonds: Vec<BondEntry>,
48    /// adjacency[atom_idx] = list of (neighbor_atom_idx, bond_idx)
49    adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
50    /// Enhanced stereo groups (ChemDraw V3000 Absolute / Or / And).
51    stereo_groups: Vec<StereoGroup>,
52    /// SMILES-text-order neighbor sequence for chiral atoms.
53    ///
54    /// Keyed by atom index.  Each value lists the atom indices of neighbors in
55    /// the order they appeared in the SMILES string (including ring-closure
56    /// partners), with [`STEREO_H_SENTINEL`] (`u32::MAX`) standing in for the
57    /// implicit bracket H.  Populated by the SMILES parser; absent for atoms
58    /// not parsed from SMILES or without recorded stereo.
59    stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
60    /// Directional (`/`, `\`) bond marker, stashed for bonds whose `order` was
61    /// overwritten to `Aromatic` (e.g. an exocyclic C=N adjacent to an
62    /// aromatic ring atom — `order` must stay `Aromatic` for SMARTS `:a`
63    /// matching, but the E/Z direction would otherwise be lost). Keyed by
64    /// bond index; value is `BondOrder::Up` or `BondOrder::Down`. Absent for
65    /// bonds whose direction is already carried directly by `order`.
66    bond_directions: std::collections::HashMap<u32, BondOrder>,
67}
68
69impl Molecule {
70    /// Number of heavy atoms (does not count implicit H).
71    pub fn atom_count(&self) -> usize {
72        self.atoms.len()
73    }
74
75    /// Number of bonds (edges).
76    pub fn bond_count(&self) -> usize {
77        self.bonds.len()
78    }
79
80    /// Borrow atom by index.
81    ///
82    /// # Panics
83    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
84    ///
85    /// For a non-panicking variant, use [`Self::atom_opt`].
86    pub fn atom(&self, idx: AtomIdx) -> &Atom {
87        let i = idx.0 as usize;
88        if i >= self.atoms.len() {
89            panic!(
90                "atom index {} out of range (molecule has {} atoms)",
91                idx.0,
92                self.atoms.len()
93            );
94        }
95        &self.atoms[i]
96    }
97
98    /// Borrow atom by index, returning `None` if out of range.
99    pub fn atom_opt(&self, idx: AtomIdx) -> Option<&Atom> {
100        let i = idx.0 as usize;
101        if i < self.atoms.len() {
102            Some(&self.atoms[i])
103        } else {
104            None
105        }
106    }
107
108    /// Borrow bond by index.
109    ///
110    /// # Panics
111    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
112    ///
113    /// For a non-panicking variant, use [`Self::bond_opt`].
114    pub fn bond(&self, idx: BondIdx) -> &BondEntry {
115        let i = idx.0 as usize;
116        if i >= self.bonds.len() {
117            panic!(
118                "bond index {} out of range (molecule has {} bonds)",
119                idx.0,
120                self.bonds.len()
121            );
122        }
123        &self.bonds[i]
124    }
125
126    /// Borrow bond by index, returning `None` if out of range.
127    pub fn bond_opt(&self, idx: BondIdx) -> Option<&BondEntry> {
128        let i = idx.0 as usize;
129        if i < self.bonds.len() {
130            Some(&self.bonds[i])
131        } else {
132            None
133        }
134    }
135
136    /// Iterate over all atoms as `(AtomIdx, &Atom)`.
137    pub fn atoms(&self) -> impl Iterator<Item = (AtomIdx, &Atom)> {
138        self.atoms
139            .iter()
140            .enumerate()
141            .map(|(i, a)| (AtomIdx(i as u32), a))
142    }
143
144    /// Iterate over all bonds as `(BondIdx, &BondEntry)`.
145    pub fn bonds(&self) -> impl Iterator<Item = (BondIdx, &BondEntry)> {
146        self.bonds
147            .iter()
148            .enumerate()
149            .map(|(i, b)| (BondIdx(i as u32), b))
150    }
151
152    /// Iterate over neighbors of `idx` as `(neighbor_atom_idx, bond_idx)`.
153    ///
154    /// # Panics
155    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
156    ///
157    /// For a non-panicking variant, use [`Self::neighbors_opt`].
158    pub fn neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (AtomIdx, BondIdx)> + '_ {
159        let i = idx.0 as usize;
160        if i >= self.adjacency.len() {
161            panic!(
162                "atom index {} out of range (molecule has {} atoms)",
163                idx.0,
164                self.adjacency.len()
165            );
166        }
167        self.adjacency[i].iter().copied()
168    }
169
170    /// Iterate over neighbors of `idx` as `(neighbor_atom_idx, bond_idx)`, returning `None` if out of range.
171    pub fn neighbors_opt(&self, idx: AtomIdx) -> Option<Vec<(AtomIdx, BondIdx)>> {
172        let i = idx.0 as usize;
173        if i < self.adjacency.len() {
174            Some(self.adjacency[i].to_vec())
175        } else {
176            None
177        }
178    }
179
180    /// Degree (number of connected bonds) of atom `idx`.
181    ///
182    /// # Panics
183    /// Panics if `idx` is out of range (should not happen with indices from this molecule).
184    ///
185    /// For a non-panicking variant, use [`Self::degree_opt`].
186    pub fn degree(&self, idx: AtomIdx) -> usize {
187        let i = idx.0 as usize;
188        if i >= self.adjacency.len() {
189            panic!(
190                "atom index {} out of range (molecule has {} atoms)",
191                idx.0,
192                self.adjacency.len()
193            );
194        }
195        self.adjacency[i].len()
196    }
197
198    /// Degree (number of connected bonds) of atom `idx`, returning `None` if out of range.
199    pub fn degree_opt(&self, idx: AtomIdx) -> Option<usize> {
200        let i = idx.0 as usize;
201        if i < self.adjacency.len() {
202            Some(self.adjacency[i].len())
203        } else {
204            None
205        }
206    }
207
208    /// Return the bond between `a` and `b`, or `None` if not connected or indices are out of bounds.
209    pub fn bond_between(&self, a: AtomIdx, b: AtomIdx) -> Option<(BondIdx, &BondEntry)> {
210        let a_idx = a.0 as usize;
211        let b_idx = b.0 as usize;
212        if a_idx >= self.adjacency.len() || b_idx >= self.atoms.len() {
213            return None;
214        }
215        self.adjacency[a_idx]
216            .iter()
217            .find(|&&(nb, _)| nb == b)
218            .and_then(|&(_, bidx)| {
219                let bond_idx = bidx.0 as usize;
220                if bond_idx < self.bonds.len() {
221                    Some((bidx, &self.bonds[bond_idx]))
222                } else {
223                    None
224                }
225            })
226    }
227
228    /// Molecular formula as a Hill-order string (C first, H second, then alphabetical).
229    pub fn formula(&self) -> String {
230        use std::collections::BTreeMap;
231        let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
232        for (_, atom) in self.atoms() {
233            *counts.entry(atom.element.symbol()).or_insert(0) += 1;
234        }
235        let mut result = Self::format_hill_order_formula(&counts);
236        let total_charge: i32 = self.atoms().map(|(_, a)| a.charge as i32).sum();
237        match total_charge {
238            0 => {}
239            1 => result.push('+'),
240            -1 => result.push('-'),
241            n if n > 0 => result.push_str(&format!("+{n}")),
242            n => result.push_str(&n.to_string()),
243        }
244        result
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Immutable update methods (functional-style editing)
250// ---------------------------------------------------------------------------
251
252impl Molecule {
253    /// Format element counts in Hill order: C, H, then alphabetically.
254    fn format_hill_order_formula(counts: &std::collections::BTreeMap<&str, u32>) -> String {
255        let mut counts = counts.clone();
256        let mut result = String::new();
257        let push_count = |sym: &str, n: u32, out: &mut String| {
258            out.push_str(sym);
259            if n > 1 {
260                out.push_str(&n.to_string());
261            }
262        };
263        if let Some(c) = counts.remove("C") {
264            push_count("C", c, &mut result);
265        }
266        if let Some(h) = counts.remove("H")
267            && h > 0
268        {
269            push_count("H", h, &mut result);
270        }
271        for (sym, count) in &counts {
272            push_count(sym, *count, &mut result);
273        }
274        result
275    }
276
277    /// Return a new `Molecule` with one extra atom appended, along with the
278    /// index that the new atom will have in the returned molecule.
279    pub fn with_atom_added(&self, atom: Atom) -> (Molecule, AtomIdx) {
280        let mut builder = MoleculeBuilder::from_molecule(self);
281        let new_idx = builder.add_atom(atom);
282        (builder.build(), new_idx)
283    }
284
285    /// Return a new `Molecule` with one extra bond added, along with the index
286    /// of the newly added bond in the returned molecule.
287    ///
288    /// Returns `Err` if `a == b` or the bond already exists (same semantics as
289    /// [`MoleculeBuilder::add_bond`]).
290    pub fn with_bond_added(
291        &self,
292        a: AtomIdx,
293        b: AtomIdx,
294        order: BondOrder,
295    ) -> Result<(Molecule, BondIdx), MolError> {
296        let mut builder = MoleculeBuilder::from_molecule(self);
297        let bond_idx = builder.add_bond(a, b, order)?;
298        Ok((builder.build(), bond_idx))
299    }
300
301    /// Return a new `Molecule` with the formal charge of atom `idx` changed.
302    pub fn with_atom_charge(&self, idx: AtomIdx, charge: i8) -> Molecule {
303        let mut builder = MoleculeBuilder::new();
304        for (aidx, atom) in self.atoms() {
305            let mut a = atom.clone();
306            if aidx == idx {
307                a.charge = charge;
308            }
309            builder.add_atom(a);
310        }
311        for (_, bond) in self.bonds() {
312            let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
313        }
314        builder.copy_stereo_from(self);
315        builder.copy_bond_directions_from(self);
316        builder.build()
317    }
318
319    /// Return a new `Molecule` with the element of atom `idx` changed.
320    ///
321    /// Chirality and hydrogen count are reset to `None` when the element
322    /// changes, since those properties are element-specific.
323    pub fn with_atom_element(&self, idx: AtomIdx, el: Element) -> Molecule {
324        let mut builder = MoleculeBuilder::new();
325        for (aidx, atom) in self.atoms() {
326            let mut a = atom.clone();
327            if aidx == idx {
328                a.element = el;
329                // Reset element-specific fields so valence stays consistent.
330                a.chirality = crate::atom::Chirality::None;
331                a.hydrogen_count = None;
332                a.aromatic = false;
333            }
334            builder.add_atom(a);
335        }
336        for (_, bond) in self.bonds() {
337            let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
338        }
339        builder.copy_stereo_from(self);
340        builder.copy_bond_directions_from(self);
341        // Chirality was cleared for the changed atom; remove its stereo order too.
342        builder.clear_stereo_neighbor_order(idx);
343        builder.build()
344    }
345
346    /// Return a new `Molecule` with atom `idx` and all bonds involving it
347    /// removed.  Atom indices of survivors shift down past the removed slot.
348    ///
349    /// The returned tuple also includes a mapping from **old** `AtomIdx` to
350    /// **new** `AtomIdx` (indices that fall below `idx` are unchanged; indices
351    /// above `idx` decrease by 1).
352    pub fn with_atom_removed(&self, idx: AtomIdx) -> (Molecule, Vec<Option<AtomIdx>>) {
353        let n = self.atom_count();
354        let removed = idx.0 as usize;
355
356        // Build old→new index table.
357        let mut remap: Vec<Option<AtomIdx>> = vec![None; n];
358        let mut new_pos = 0u32;
359        for (old, slot) in remap.iter_mut().enumerate() {
360            if old == removed {
361                continue;
362            }
363            *slot = Some(AtomIdx(new_pos));
364            new_pos += 1;
365        }
366
367        let mut builder = MoleculeBuilder::new();
368        for (aidx, atom) in self.atoms() {
369            if aidx == idx {
370                continue;
371            }
372            builder.add_atom(atom.clone());
373        }
374        for (_, bond) in self.bonds() {
375            if bond.atom1 == idx || bond.atom2 == idx {
376                continue;
377            }
378            if let (Some(a1), Some(a2)) =
379                (remap[bond.atom1.0 as usize], remap[bond.atom2.0 as usize])
380            {
381                let _ = builder.add_bond(a1, a2, bond.order);
382            }
383        }
384        // Remap stereo neighbor order: drop removed atom's entry, remap neighbor indices.
385        for (old_key, order) in &self.stereo_neighbor_order {
386            let old_atom = *old_key as usize;
387            if old_atom == removed {
388                continue; // removed atom's stereo is gone
389            }
390            if let Some(Some(new_key)) = remap.get(old_atom) {
391                let new_order: Vec<u32> = order
392                    .iter()
393                    .filter_map(|&v| {
394                        if v == STEREO_H_SENTINEL {
395                            Some(STEREO_H_SENTINEL)
396                        } else if v as usize == removed {
397                            None // neighbor was the removed atom — stereo is now invalid
398                        } else {
399                            remap.get(v as usize).and_then(|r| r.map(|a| a.0))
400                        }
401                    })
402                    .collect();
403                builder.set_stereo_neighbor_order(*new_key, new_order);
404            }
405        }
406        (builder.build(), remap)
407    }
408
409    /// Implicit hydrogen count for atom `idx` based on valence rules.
410    ///
411    /// Delegates to [`crate::valence::implicit_hcount`].
412    pub fn implicit_hydrogen_count(&self, idx: AtomIdx) -> u8 {
413        crate::valence::implicit_hcount(self, idx)
414    }
415
416    /// Hill-order molecular formula including implicit hydrogens.
417    ///
418    /// Unlike [`Self::formula`] (which counts only explicit heavy atoms),
419    /// this method adds the implicit H count for every atom so the result
420    /// reflects the true molecular composition (e.g. methane → "CH4").
421    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    /// Hill-order molecular formula with isotope labels.
434    ///
435    /// Like [`Self::formula`] but prefixes each element symbol with its
436    /// isotope number when `atom.isotope` is `Some(n)`.
437    /// Example: a molecule with one `¹³C` and one `O` → `"¹³CO"`.
438    pub fn formula_with_isotopes(&self) -> String {
439        use std::collections::BTreeMap;
440        // Collect (isotope_prefix + symbol) counts, heavy atoms only.
441        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        // Hill order: C first (if unlabelled C present), then H, then rest alphabetically.
468        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    /// Return a new `Molecule` with atom `idx`'s aromatic flag changed.
481    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    /// Return a new `Molecule` with bond `idx`'s order changed.
499    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    /// Return a new `Molecule` with bond `idx` removed.
514    ///
515    /// Atom indices are unchanged.  Bond indices of survivors shift down, so
516    /// `bond_directions` (keyed by bond index) is remapped bond-by-bond
517    /// rather than copied wholesale — `copy_bond_directions_from` would
518    /// misattribute directions to the wrong bond for every survivor after
519    /// the removed one.
520    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
540// ---------------------------------------------------------------------------
541// In-place mutation methods
542// ---------------------------------------------------------------------------
543
544impl Molecule {
545    /// Append a new atom and return its index.
546    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    /// Remove atom `idx` and all bonds involving it.
554    ///
555    /// Returns a remapping table: `remap[old_idx]` gives the new `AtomIdx`
556    /// for surviving atoms, or `None` for the removed atom.  Atom indices
557    /// of atoms after the removed slot shift down by 1.
558    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        // Keep only bonds not involving the removed atom; remap endpoints and
575        // track each surviving bond's new index so `bond_directions` (keyed
576        // by bond index) can be remapped the same way as `stereo_neighbor_order`
577        // is remapped by atom index below.
578        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        // Remap bond directions in-place, same shift as bond_remap above.
598        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        // Rebuild adjacency from scratch.
606        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        // Remap stereo neighbor order in-place.
615        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    /// Add a bond between `a` and `b` with the given `order`.
642    ///
643    /// Returns `Err` if `a == b` or the bond already exists.
644    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    /// Remove bond `idx`.  Atom indices are unchanged; bond indices of
672    /// surviving bonds shift down past the removed slot.
673    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        // Rebuild adjacency with renumbered bond indices.
680        let n = self.atoms.len();
681        self.adjacency = vec![vec![]; n];
682        for (bidx, bond) in self.bonds.iter().enumerate() {
683            let bi = BondIdx(bidx as u32);
684            self.adjacency[bond.atom1.0 as usize].push((bond.atom2, bi));
685            self.adjacency[bond.atom2.0 as usize].push((bond.atom1, bi));
686        }
687    }
688
689    /// Set the formal charge of atom `idx` in-place.
690    pub fn set_charge(&mut self, idx: AtomIdx, charge: i8) {
691        self.atoms[idx.0 as usize].charge = charge;
692    }
693
694    /// Set the element of atom `idx` in-place.
695    ///
696    /// Chirality and hydrogen count are reset (element-specific properties).
697    pub fn set_element(&mut self, idx: AtomIdx, el: Element) {
698        let a = &mut self.atoms[idx.0 as usize];
699        a.element = el;
700        a.chirality = crate::atom::Chirality::None;
701        a.hydrogen_count = None;
702        a.aromatic = false;
703    }
704
705    /// Set the CIP stereo code of atom `idx` in-place.
706    pub fn set_cip_code(&mut self, idx: AtomIdx, code: Option<crate::atom::CipCode>) {
707        self.atoms[idx.0 as usize].cip_code = code;
708    }
709
710    /// Return the enhanced stereo groups attached to this molecule.
711    pub fn stereo_groups(&self) -> &[StereoGroup] {
712        &self.stereo_groups
713    }
714
715    /// Replace the stereo group list in-place.
716    pub fn set_stereo_groups(&mut self, groups: Vec<StereoGroup>) {
717        self.stereo_groups = groups;
718    }
719
720    /// Add a single stereo group in-place.
721    pub fn add_stereo_group(&mut self, group: StereoGroup) {
722        self.stereo_groups.push(group);
723    }
724
725    /// SMILES-text-order neighbor sequence for a chiral atom.
726    ///
727    /// Returns `None` for atoms not parsed from SMILES or without stereo.
728    /// The slice contains neighbor atom indices in SMILES text order;
729    /// [`STEREO_H_SENTINEL`] (`u32::MAX`) marks the implicit bracket-H slot.
730    pub fn stereo_neighbor_order(&self, idx: AtomIdx) -> Option<&[u32]> {
731        self.stereo_neighbor_order.get(&idx.0).map(|v| v.as_slice())
732    }
733
734    /// Set the SMILES stereo neighbor order for atom `idx`.
735    pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
736        self.stereo_neighbor_order.insert(idx.0, order);
737    }
738
739    /// Directional (`/`, `\`) marker stashed for bond `idx`, if its `order`
740    /// was overwritten to `Aromatic` while it still carried E/Z direction.
741    /// Returns `BondOrder::Up` or `BondOrder::Down` when present.
742    pub fn bond_direction(&self, idx: BondIdx) -> Option<BondOrder> {
743        self.bond_directions.get(&idx.0).copied()
744    }
745
746    /// Stash a directional marker for bond `idx` (see [`Self::bond_direction`]).
747    pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
748        self.bond_directions.insert(idx.0, direction);
749    }
750}
751
752// ---------------------------------------------------------------------------
753// Connectivity utilities
754// ---------------------------------------------------------------------------
755
756impl Molecule {
757    /// Return `true` if the molecule has exactly one connected component
758    /// (i.e. every atom can be reached from every other atom).
759    pub fn is_connected(&self) -> bool {
760        let n = self.atoms.len();
761        if n == 0 {
762            return true;
763        }
764        let mut visited = vec![false; n];
765        let mut stack = vec![AtomIdx(0)];
766        visited[0] = true;
767        let mut count = 1;
768        while let Some(cur) = stack.pop() {
769            for (nb, _) in self.neighbors(cur) {
770                if !visited[nb.0 as usize] {
771                    visited[nb.0 as usize] = true;
772                    count += 1;
773                    stack.push(nb);
774                }
775            }
776        }
777        count == n
778    }
779
780    /// Split the molecule into its connected components.
781    ///
782    /// Returns a `Vec` of sub-molecules, one per component.  Atoms are
783    /// renumbered within each sub-molecule starting at index 0.
784    pub fn fragments(&self) -> Vec<Molecule> {
785        let n = self.atoms.len();
786        if n == 0 {
787            return vec![];
788        }
789
790        let mut component: Vec<usize> = vec![usize::MAX; n];
791        let mut comp_id = 0;
792
793        for start in 0..n {
794            if component[start] != usize::MAX {
795                continue;
796            }
797            let mut stack = vec![start];
798            component[start] = comp_id;
799            while let Some(cur) = stack.pop() {
800                for (nb, _) in self.neighbors(AtomIdx(cur as u32)) {
801                    let ni = nb.0 as usize;
802                    if component[ni] == usize::MAX {
803                        component[ni] = comp_id;
804                        stack.push(ni);
805                    }
806                }
807            }
808            comp_id += 1;
809        }
810
811        (0..comp_id)
812            .map(|cid| {
813                let mut builder = MoleculeBuilder::new();
814                let mut old_to_new: std::collections::HashMap<AtomIdx, AtomIdx> =
815                    std::collections::HashMap::new();
816                for (aidx, atom) in self.atoms() {
817                    if component[aidx.0 as usize] == cid {
818                        let new_idx = builder.add_atom(atom.clone());
819                        old_to_new.insert(aidx, new_idx);
820                    }
821                }
822                for (_, bond) in self.bonds() {
823                    if let (Some(&a1), Some(&a2)) =
824                        (old_to_new.get(&bond.atom1), old_to_new.get(&bond.atom2))
825                    {
826                        let _ = builder.add_bond(a1, a2, bond.order);
827                    }
828                }
829                builder.build()
830            })
831            .collect()
832    }
833}
834
835/// Builder for constructing a [`Molecule`] incrementally.
836///
837/// Usage: add atoms, add bonds, then call `build()`.
838#[derive(Default)]
839pub struct MoleculeBuilder {
840    atoms: Vec<Atom>,
841    bonds: Vec<BondEntry>,
842    adjacency: Vec<Vec<(AtomIdx, BondIdx)>>,
843    stereo_groups: Vec<StereoGroup>,
844    stereo_neighbor_order: std::collections::HashMap<u32, Vec<u32>>,
845    bond_directions: std::collections::HashMap<u32, BondOrder>,
846}
847
848impl MoleculeBuilder {
849    pub fn new() -> Self {
850        Self::default()
851    }
852
853    /// Create a builder pre-populated with all atoms and bonds from `mol`.
854    ///
855    /// Use this to make incremental edits to an existing molecule instead of
856    /// reconstructing it from scratch.
857    pub fn from_molecule(mol: &Molecule) -> Self {
858        let mut b = Self::new();
859        for (_, atom) in mol.atoms() {
860            b.add_atom(atom.clone());
861        }
862        for (_, bond) in mol.bonds() {
863            let _ = b.add_bond(bond.atom1, bond.atom2, bond.order);
864        }
865        b.stereo_groups = mol.stereo_groups.clone();
866        b.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
867        b.bond_directions = mol.bond_directions.clone();
868        b
869    }
870
871    /// Set the SMILES stereo neighbor order for atom `idx`.
872    pub fn set_stereo_neighbor_order(&mut self, idx: AtomIdx, order: Vec<u32>) {
873        self.stereo_neighbor_order.insert(idx.0, order);
874    }
875
876    /// Remove the stereo neighbor order entry for atom `idx`.
877    pub fn clear_stereo_neighbor_order(&mut self, idx: AtomIdx) {
878        self.stereo_neighbor_order.remove(&idx.0);
879    }
880
881    /// Append a stereo group to this builder.
882    pub fn add_stereo_group(&mut self, group: StereoGroup) {
883        self.stereo_groups.push(group);
884    }
885
886    /// Copy all enhanced stereo groups from `mol` into this builder verbatim.
887    ///
888    /// Only valid when atom indices are unchanged from `mol` (atoms re-added
889    /// in the same order, none removed) — same caveat as
890    /// [`Self::copy_bond_directions_from`].
891    pub fn copy_stereo_groups_from(&mut self, mol: &Molecule) {
892        self.stereo_groups = mol.stereo_groups.clone();
893    }
894
895    /// Copy all stereo neighbor order entries from `mol` into this builder.
896    pub fn copy_stereo_from(&mut self, mol: &Molecule) {
897        self.stereo_neighbor_order = mol.stereo_neighbor_order.clone();
898    }
899
900    /// Stash a directional marker for bond `idx` (see [`Molecule::bond_direction`]).
901    pub fn set_bond_direction(&mut self, idx: BondIdx, direction: BondOrder) {
902        self.bond_directions.insert(idx.0, direction);
903    }
904
905    /// Copy all bond-direction entries from `mol` into this builder verbatim.
906    ///
907    /// Only valid when bond indices are unchanged from `mol` (atoms/bonds
908    /// re-added in the same order, none skipped) — e.g. a rebuild that only
909    /// touches atom fields or promotes bond order to `Aromatic`. A rebuild
910    /// that removes or reorders bonds must remap directions bond-by-bond
911    /// instead (see `Molecule::with_bond_removed`).
912    pub fn copy_bond_directions_from(&mut self, mol: &Molecule) {
913        self.bond_directions = mol.bond_directions.clone();
914    }
915
916    /// Read-only reference to an atom already added to the builder.
917    ///
918    /// Used by the SMILES parser to infer implicit bond types without
919    /// consuming the builder (e.g. aromatic-aromatic → Aromatic bond).
920    ///
921    /// # Panics
922    /// Panics if `idx` is out of range.
923    pub fn atom_at(&self, idx: AtomIdx) -> &Atom {
924        &self.atoms[idx.0 as usize]
925    }
926
927    /// Number of atoms added so far.
928    pub fn atom_count(&self) -> usize {
929        self.atoms.len()
930    }
931
932    /// Iterate over already-added neighbors of `idx` as `(bond_idx, neighbor_atom_idx)`.
933    /// Used by kekulization tests to check whether a bond already exists in the builder.
934    pub fn atom_neighbors(&self, idx: AtomIdx) -> impl Iterator<Item = (BondIdx, AtomIdx)> + '_ {
935        self.adjacency[idx.0 as usize]
936            .iter()
937            .map(|&(nb, bidx)| (bidx, nb))
938    }
939
940    /// Add an atom and return its index.
941    pub fn add_atom(&mut self, atom: Atom) -> AtomIdx {
942        let idx = AtomIdx(self.atoms.len() as u32);
943        self.atoms.push(atom);
944        self.adjacency.push(Vec::new());
945        idx
946    }
947
948    /// Add a bond between two existing atoms.
949    ///
950    /// Returns an error if either atom index is invalid or if the bond already exists.
951    pub fn add_bond(
952        &mut self,
953        a: AtomIdx,
954        b: AtomIdx,
955        order: BondOrder,
956    ) -> Result<BondIdx, MolError> {
957        let n = self.atoms.len() as u32;
958        if a.0 >= n {
959            return Err(MolError::InvalidAtomIdx(a));
960        }
961        if b.0 >= n {
962            return Err(MolError::InvalidAtomIdx(b));
963        }
964
965        // Check for duplicate
966        for &(nb, _) in &self.adjacency[a.0 as usize] {
967            if nb == b {
968                return Err(MolError::DuplicateBond(a, b));
969            }
970        }
971
972        let bidx = BondIdx(self.bonds.len() as u32);
973        self.bonds.push(BondEntry {
974            atom1: a,
975            atom2: b,
976            order,
977        });
978        self.adjacency[a.0 as usize].push((b, bidx));
979        self.adjacency[b.0 as usize].push((a, bidx));
980        Ok(bidx)
981    }
982
983    /// Consume the builder and return an immutable [`Molecule`].
984    pub fn build(self) -> Molecule {
985        Molecule {
986            atoms: self.atoms,
987            bonds: self.bonds,
988            adjacency: self.adjacency,
989            stereo_groups: self.stereo_groups,
990            stereo_neighbor_order: self.stereo_neighbor_order,
991            bond_directions: self.bond_directions,
992        }
993    }
994}
995
996#[cfg(test)]
997mod tests {
998    use super::*;
999    use crate::atom::Atom;
1000    use crate::element::Element;
1001
1002    fn ethane() -> Molecule {
1003        let mut b = MoleculeBuilder::new();
1004        let c1 = b.add_atom(Atom::new(Element::C));
1005        let c2 = b.add_atom(Atom::new(Element::C));
1006        b.add_bond(c1, c2, BondOrder::Single).unwrap();
1007        b.build()
1008    }
1009
1010    #[test]
1011    fn test_basic_molecule() {
1012        let mol = ethane();
1013        assert_eq!(mol.atom_count(), 2);
1014        assert_eq!(mol.bond_count(), 1);
1015    }
1016
1017    #[test]
1018    fn test_adjacency() {
1019        let mol = ethane();
1020        let neighbors: Vec<_> = mol.neighbors(AtomIdx(0)).collect();
1021        assert_eq!(neighbors.len(), 1);
1022        assert_eq!(neighbors[0].0, AtomIdx(1));
1023    }
1024
1025    #[test]
1026    fn test_bond_between() {
1027        let mol = ethane();
1028        assert!(mol.bond_between(AtomIdx(0), AtomIdx(1)).is_some());
1029        assert!(mol.bond_between(AtomIdx(1), AtomIdx(0)).is_some());
1030    }
1031
1032    #[test]
1033    fn test_duplicate_bond_error() {
1034        let mut b = MoleculeBuilder::new();
1035        let c1 = b.add_atom(Atom::new(Element::C));
1036        let c2 = b.add_atom(Atom::new(Element::C));
1037        b.add_bond(c1, c2, BondOrder::Single).unwrap();
1038        let err = b.add_bond(c1, c2, BondOrder::Double);
1039        assert!(matches!(err, Err(MolError::DuplicateBond(_, _))));
1040    }
1041
1042    #[test]
1043    fn test_formula() {
1044        let mut b = MoleculeBuilder::new();
1045        let c = b.add_atom(Atom::new(Element::C));
1046        let n = b.add_atom(Atom::new(Element::N));
1047        b.add_bond(c, n, BondOrder::Single).unwrap();
1048        let mol = b.build();
1049        assert_eq!(mol.formula(), "CN");
1050    }
1051
1052    #[test]
1053    fn test_implicit_hydrogen_count() {
1054        // Isolated C atom (sp3, 4 bonds available): 4 implicit H
1055        let mut b = MoleculeBuilder::new();
1056        b.add_atom(Atom::organic(Element::C));
1057        let mol = b.build();
1058        assert_eq!(mol.implicit_hydrogen_count(AtomIdx(0)), 4);
1059    }
1060
1061    #[test]
1062    fn test_total_formula_methane() {
1063        // Organic C atom with 0 explicit bonds → 4 implicit H → CH4
1064        let mut b = MoleculeBuilder::new();
1065        b.add_atom(Atom::organic(Element::C));
1066        let mol = b.build();
1067        assert_eq!(mol.total_formula(), "CH4");
1068    }
1069
1070    #[test]
1071    fn test_total_formula_no_hydrogen() {
1072        // NaCl — neither Na nor Cl is in the organic subset, no implicit H
1073        let mut b = MoleculeBuilder::new();
1074        let na = b.add_atom(Atom::new(Element::NA));
1075        let cl = b.add_atom(Atom::new(Element::CL));
1076        b.add_bond(na, cl, BondOrder::Single).unwrap();
1077        let mol = b.build();
1078        assert_eq!(mol.total_formula(), "ClNa");
1079    }
1080
1081    #[test]
1082    fn test_with_atom_aromatic() {
1083        let mol = ethane();
1084        let updated = mol.with_atom_aromatic(AtomIdx(0), true);
1085        assert!(updated.atom(AtomIdx(0)).aromatic);
1086        assert!(!updated.atom(AtomIdx(1)).aromatic);
1087    }
1088
1089    #[test]
1090    fn test_with_bond_order() {
1091        let mol = ethane();
1092        let updated = mol.with_bond_order(BondIdx(0), BondOrder::Double);
1093        assert_eq!(updated.bond(BondIdx(0)).order, BondOrder::Double);
1094    }
1095
1096    // --- mutable API ---
1097
1098    #[test]
1099    fn test_add_remove_atom() {
1100        let mut mol = ethane();
1101        let n_idx = mol.add_atom(Atom::new(Element::N));
1102        assert_eq!(mol.atom_count(), 3);
1103        assert_eq!(mol.atom(n_idx).element.atomic_number(), 7);
1104
1105        let remap = mol.remove_atom(n_idx);
1106        assert_eq!(mol.atom_count(), 2);
1107        assert!(remap[n_idx.0 as usize].is_none());
1108    }
1109
1110    #[test]
1111    fn test_add_remove_bond() {
1112        let mut mol = ethane();
1113        let n_idx = mol.add_atom(Atom::new(Element::N));
1114        let bidx = mol.add_bond(AtomIdx(0), n_idx, BondOrder::Single).unwrap();
1115        assert_eq!(mol.bond_count(), 2);
1116        mol.remove_bond(bidx);
1117        assert_eq!(mol.bond_count(), 1);
1118    }
1119
1120    #[test]
1121    fn test_set_charge_element() {
1122        let mut mol = ethane();
1123        mol.set_charge(AtomIdx(0), 1);
1124        assert_eq!(mol.atom(AtomIdx(0)).charge, 1);
1125        mol.set_element(AtomIdx(0), Element::N);
1126        assert_eq!(mol.atom(AtomIdx(0)).element.atomic_number(), 7);
1127    }
1128
1129    #[test]
1130    fn test_is_connected() {
1131        let mol = ethane();
1132        assert!(mol.is_connected());
1133
1134        // Two separate atoms — disconnected
1135        let mut b = MoleculeBuilder::new();
1136        b.add_atom(Atom::new(Element::C));
1137        b.add_atom(Atom::new(Element::N));
1138        let disconnected = b.build();
1139        assert!(!disconnected.is_connected());
1140    }
1141
1142    #[test]
1143    fn test_fragments() {
1144        // "CC.N" — two components
1145        let mut b = MoleculeBuilder::new();
1146        let c1 = b.add_atom(Atom::organic(Element::C));
1147        let c2 = b.add_atom(Atom::organic(Element::C));
1148        b.add_bond(c1, c2, BondOrder::Single).unwrap();
1149        b.add_atom(Atom::new(Element::N)); // disconnected N
1150        let mol = b.build();
1151        let frags = mol.fragments();
1152        assert_eq!(frags.len(), 2);
1153        let sizes: std::collections::HashSet<usize> =
1154            frags.iter().map(|f| f.atom_count()).collect();
1155        assert!(sizes.contains(&2));
1156        assert!(sizes.contains(&1));
1157    }
1158
1159    #[test]
1160    fn test_builder_from_molecule() {
1161        let mol = ethane();
1162        let mut b = MoleculeBuilder::from_molecule(&mol);
1163        b.add_atom(Atom::new(Element::O));
1164        let mol2 = b.build();
1165        assert_eq!(mol2.atom_count(), 3);
1166        assert_eq!(mol2.bond_count(), 1); // original bond preserved
1167    }
1168
1169    // --- safe Option-returning variants ---
1170
1171    #[test]
1172    fn test_atom_opt_valid() {
1173        let mol = ethane();
1174        assert!(mol.atom_opt(AtomIdx(0)).is_some());
1175        assert!(mol.atom_opt(AtomIdx(1)).is_some());
1176        let atom = mol.atom_opt(AtomIdx(0)).unwrap();
1177        assert_eq!(atom.element.atomic_number(), 6);
1178    }
1179
1180    #[test]
1181    fn test_atom_opt_invalid() {
1182        let mol = ethane();
1183        assert!(mol.atom_opt(AtomIdx(2)).is_none());
1184        assert!(mol.atom_opt(AtomIdx(1000)).is_none());
1185    }
1186
1187    #[test]
1188    fn test_bond_opt_valid() {
1189        let mol = ethane();
1190        assert!(mol.bond_opt(BondIdx(0)).is_some());
1191        let bond = mol.bond_opt(BondIdx(0)).unwrap();
1192        assert_eq!(bond.order, BondOrder::Single);
1193    }
1194
1195    #[test]
1196    fn test_bond_opt_invalid() {
1197        let mol = ethane();
1198        assert!(mol.bond_opt(BondIdx(1)).is_none());
1199        assert!(mol.bond_opt(BondIdx(1000)).is_none());
1200    }
1201
1202    #[test]
1203    fn test_neighbors_opt_valid() {
1204        let mol = ethane();
1205        let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1206        assert_eq!(neighbors.len(), 1);
1207        assert_eq!(neighbors[0].0, AtomIdx(1));
1208    }
1209
1210    #[test]
1211    fn test_neighbors_opt_isolated_atom() {
1212        let mut b = MoleculeBuilder::new();
1213        b.add_atom(Atom::new(Element::C));
1214        b.add_atom(Atom::new(Element::N));
1215        let mol = b.build();
1216        let neighbors = mol.neighbors_opt(AtomIdx(0)).unwrap();
1217        assert_eq!(neighbors.len(), 0);
1218    }
1219
1220    #[test]
1221    fn test_neighbors_opt_invalid() {
1222        let mol = ethane();
1223        assert!(mol.neighbors_opt(AtomIdx(2)).is_none());
1224        assert!(mol.neighbors_opt(AtomIdx(1000)).is_none());
1225    }
1226
1227    #[test]
1228    fn test_degree_opt_valid() {
1229        let mol = ethane();
1230        assert_eq!(mol.degree_opt(AtomIdx(0)), Some(1));
1231        assert_eq!(mol.degree_opt(AtomIdx(1)), Some(1));
1232    }
1233
1234    #[test]
1235    fn test_degree_opt_isolated_atom() {
1236        let mut b = MoleculeBuilder::new();
1237        b.add_atom(Atom::new(Element::C));
1238        b.add_atom(Atom::new(Element::N));
1239        let mol = b.build();
1240        assert_eq!(mol.degree_opt(AtomIdx(0)), Some(0));
1241        assert_eq!(mol.degree_opt(AtomIdx(1)), Some(0));
1242    }
1243
1244    #[test]
1245    fn test_degree_opt_invalid() {
1246        let mol = ethane();
1247        assert!(mol.degree_opt(AtomIdx(2)).is_none());
1248        assert!(mol.degree_opt(AtomIdx(1000)).is_none());
1249    }
1250
1251    #[test]
1252    fn test_degree_opt_multiple_bonds() {
1253        // Create a central atom with 3 neighbors
1254        let mut b = MoleculeBuilder::new();
1255        let center = b.add_atom(Atom::new(Element::C));
1256        let n1 = b.add_atom(Atom::new(Element::C));
1257        let n2 = b.add_atom(Atom::new(Element::N));
1258        let n3 = b.add_atom(Atom::new(Element::O));
1259        b.add_bond(center, n1, BondOrder::Single).unwrap();
1260        b.add_bond(center, n2, BondOrder::Double).unwrap();
1261        b.add_bond(center, n3, BondOrder::Single).unwrap();
1262        let mol = b.build();
1263        assert_eq!(mol.degree_opt(center), Some(3));
1264        assert_eq!(mol.degree_opt(n1), Some(1));
1265        assert_eq!(mol.degree_opt(n2), Some(1));
1266        assert_eq!(mol.degree_opt(n3), Some(1));
1267    }
1268}