Skip to main content

chematic_core/
bond.rs

1//! Bond types and the bond-entry struct for molecular graphs.
2
3/// Bond order, covering all SMILES bond types.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum BondOrder {
6    /// Single bond `-`
7    Single,
8    /// Double bond `=`
9    Double,
10    /// Triple bond `#`
11    Triple,
12    /// Quadruple bond `$` (used in some metal complexes)
13    Quadruple,
14    /// Aromatic bond `:` (pre-Kekulization representation)
15    Aromatic,
16    /// Stereo bond `/` (E/Z specification)
17    Up,
18    /// Stereo bond `\` (E/Z specification)
19    Down,
20    /// Zero-order bond used by CTAB/CXSMILES for non-valence connections.
21    Zero,
22    /// Dative/coordinate bond. The stored `atom1 → atom2` direction is the donor→acceptor direction.
23    Dative,
24    /// Query bond matching any bond (`~` / MDL type 8).
25    QueryAny,
26    /// Query bond matching single or double bonds (MDL type 5).
27    QuerySingleOrDouble,
28    /// Query bond matching single or aromatic bonds (MDL type 6).
29    QuerySingleOrAromatic,
30    /// Query bond matching double or aromatic bonds (MDL type 7).
31    QueryDoubleOrAromatic,
32}
33
34impl BondOrder {
35    /// Fractional bond order. Returns `None` for aromatic (non-integer).
36    pub fn order_value(self) -> Option<f32> {
37        match self {
38            Self::Single => Some(1.0),
39            Self::Double => Some(2.0),
40            Self::Triple => Some(3.0),
41            Self::Quadruple => Some(4.0),
42            Self::Zero => Some(0.0),
43            Self::Dative => Some(1.0),
44            Self::Aromatic => None,
45            Self::Up | Self::Down => Some(1.0),
46            Self::QueryAny
47            | Self::QuerySingleOrDouble
48            | Self::QuerySingleOrAromatic
49            | Self::QueryDoubleOrAromatic => None,
50        }
51    }
52
53    /// Integer bond order used for valence calculations.
54    /// Aromatic, stereo, dative, and query bonds count conservatively as 1.
55    pub fn order_int(self) -> u8 {
56        match self {
57            Self::Zero => 0,
58            Self::Single
59            | Self::Up
60            | Self::Down
61            | Self::Aromatic
62            | Self::Dative
63            | Self::QueryAny
64            | Self::QuerySingleOrDouble
65            | Self::QuerySingleOrAromatic
66            | Self::QueryDoubleOrAromatic => 1,
67            Self::Double => 2,
68            Self::Triple => 3,
69            Self::Quadruple => 4,
70        }
71    }
72
73    /// The SMILES/CXSMILES token that represents this bond order when available.
74    pub fn smiles_token(self) -> &'static str {
75        match self {
76            Self::Single => "-",
77            Self::Double => "=",
78            Self::Triple => "#",
79            Self::Quadruple => "$",
80            Self::Aromatic => ":",
81            Self::Up => "/",
82            Self::Down => "\\",
83            Self::Dative => "->",
84            Self::Zero | Self::QueryAny => "~",
85            Self::QuerySingleOrDouble
86            | Self::QuerySingleOrAromatic
87            | Self::QueryDoubleOrAromatic => "~",
88        }
89    }
90
91    /// The SMILES character that represents this bond order.
92    pub fn smiles_char(self) -> char {
93        self.smiles_token().as_bytes()[0] as char
94    }
95}
96
97impl core::fmt::Display for BondOrder {
98    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        write!(f, "{}", self.smiles_token())
100    }
101}
102
103/// A single bond edge in the molecular graph.
104///
105/// The graph is undirected; `atom1`/`atom2` ordering is not guaranteed.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct BondEntry {
108    pub atom1: crate::molecule::AtomIdx,
109    pub atom2: crate::molecule::AtomIdx,
110    pub order: BondOrder,
111}
112
113impl BondEntry {
114    /// Return the atom on the other side of this bond, or `None` if `idx` is not an endpoint.
115    pub fn other(&self, idx: crate::molecule::AtomIdx) -> Option<crate::molecule::AtomIdx> {
116        if self.atom1 == idx {
117            Some(self.atom2)
118        } else if self.atom2 == idx {
119            Some(self.atom1)
120        } else {
121            None
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::molecule::AtomIdx;
130
131    #[test]
132    fn test_bond_order_values() {
133        assert_eq!(BondOrder::Single.order_value(), Some(1.0));
134        assert_eq!(BondOrder::Double.order_value(), Some(2.0));
135        assert_eq!(BondOrder::Triple.order_value(), Some(3.0));
136        assert_eq!(BondOrder::Aromatic.order_value(), None);
137    }
138
139    #[test]
140    fn test_bond_other() {
141        let b = BondEntry {
142            atom1: AtomIdx(0),
143            atom2: AtomIdx(1),
144            order: BondOrder::Single,
145        };
146        assert_eq!(b.other(AtomIdx(0)), Some(AtomIdx(1)));
147        assert_eq!(b.other(AtomIdx(1)), Some(AtomIdx(0)));
148        assert_eq!(b.other(AtomIdx(2)), None);
149    }
150}