molprint_core/mol/
graph.rs1use super::atom::Atom;
2use super::bond::BondType;
3use petgraph::graph::{NodeIndex, UnGraph};
4
5pub type MolGraph = UnGraph<Atom, BondType>;
7
8pub trait MolGraphExt {
10 fn num_atoms(&self) -> usize;
12 fn num_bonds(&self) -> usize;
14 fn atom(&self, idx: NodeIndex) -> &Atom;
16 fn atom_mut(&mut self, idx: NodeIndex) -> &mut Atom;
18 fn bond_between(&self, a: NodeIndex, b: NodeIndex) -> Option<BondType>;
20 fn heavy_neighbors(&self, idx: NodeIndex) -> Vec<NodeIndex>;
22 fn degree(&self, idx: NodeIndex) -> usize;
24
25 fn compute_implicit_h(&self, idx: NodeIndex) -> u8;
32
33 fn assign_implicit_hydrogens(&mut self);
35}
36
37impl MolGraphExt for MolGraph {
38 fn num_atoms(&self) -> usize {
39 self.node_count()
40 }
41
42 fn num_bonds(&self) -> usize {
43 self.edge_count()
44 }
45
46 fn atom(&self, idx: NodeIndex) -> &Atom {
47 &self[idx]
48 }
49
50 fn atom_mut(&mut self, idx: NodeIndex) -> &mut Atom {
51 &mut self[idx]
52 }
53
54 fn bond_between(&self, a: NodeIndex, b: NodeIndex) -> Option<BondType> {
55 self.find_edge(a, b).map(|e| self[e])
56 }
57
58 fn heavy_neighbors(&self, idx: NodeIndex) -> Vec<NodeIndex> {
59 self.neighbors(idx).collect()
60 }
61
62 fn degree(&self, idx: NodeIndex) -> usize {
63 self.neighbors(idx).count()
64 }
65
66 fn compute_implicit_h(&self, idx: NodeIndex) -> u8 {
67 let atom = &self[idx];
68
69 if let Some(h) = atom.explicit_h {
71 return h;
72 }
73
74 if !atom.element.is_organic_subset() {
79 return 0;
80 }
81
82 let bond_sum: u8 = self
84 .edges(idx)
85 .map(|e| e.weight().valence_contribution())
86 .sum();
87
88 let valences = atom.element.default_valences();
89 if valences.is_empty() || (valences.len() == 1 && valences[0] == 0) {
90 return 0;
91 }
92
93 if atom.aromatic {
94 for &v in valences {
99 if v >= bond_sum {
100 let h_raw = v.saturating_sub(bond_sum);
101 let charge_adj = atom.charge.unsigned_abs();
102 if h_raw == 0 {
103 return 0_u8.saturating_sub(charge_adj);
105 }
106 let effective_sum = bond_sum + 1;
108 for &v2 in valences {
109 if v2 >= effective_sum {
110 return v2.saturating_sub(effective_sum).saturating_sub(charge_adj);
111 }
112 }
113 return 0;
114 }
115 }
116 return 0;
117 }
118
119 for &v in valences {
121 if v >= bond_sum {
122 let charge_adj = atom.charge.unsigned_abs();
123 return v.saturating_sub(bond_sum).saturating_sub(charge_adj);
124 }
125 }
126
127 0
128 }
129
130 fn assign_implicit_hydrogens(&mut self) {
131 let indices: Vec<NodeIndex> = self.node_indices().collect();
132 for idx in indices {
133 if self[idx].element == super::atom::Element::H {
134 continue; }
136 let h_implicit = self.compute_implicit_h(idx);
137 let h_explicit: u8 = self
139 .neighbors(idx)
140 .filter(|&nb| self[nb].element == super::atom::Element::H)
141 .count()
142 .min(255) as u8;
143 self[idx].h_count = h_implicit + h_explicit;
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::super::atom::Element;
151 use super::*;
152
153 fn make_ethanol() -> MolGraph {
154 let mut g = MolGraph::new_undirected();
155 let c1 = g.add_node(Atom::new(Element::C));
156 let c2 = g.add_node(Atom::new(Element::C));
157 let o = g.add_node(Atom::new(Element::O));
158 g.add_edge(c1, c2, BondType::Single);
159 g.add_edge(c2, o, BondType::Single);
160 g.assign_implicit_hydrogens();
161 g
162 }
163
164 #[test]
165 fn test_ethanol_structure() {
166 let g = make_ethanol();
167 assert_eq!(g.num_atoms(), 3);
168 assert_eq!(g.num_bonds(), 2);
169 }
170
171 #[test]
172 fn test_ethanol_implicit_h() {
173 let g = make_ethanol();
174 assert_eq!(g[NodeIndex::new(0)].h_count, 3);
176 assert_eq!(g[NodeIndex::new(1)].h_count, 2);
177 assert_eq!(g[NodeIndex::new(2)].h_count, 1);
178 }
179
180 #[test]
181 fn test_benzene_implicit_h() {
182 let mut g = MolGraph::new_undirected();
183 let atoms: Vec<NodeIndex> = (0..6)
184 .map(|_| {
185 let mut a = Atom::new(Element::C);
186 a.aromatic = true;
187 g.add_node(a)
188 })
189 .collect();
190 for i in 0..5 {
191 g.add_edge(atoms[i], atoms[i + 1], BondType::Aromatic);
192 }
193 g.add_edge(atoms[5], atoms[0], BondType::Aromatic);
194 g.assign_implicit_hydrogens();
195 for &a in &atoms {
198 assert_eq!(g[a].h_count, 1, "aromatic C should have 1H");
199 }
200 }
201
202 #[test]
203 fn test_heavy_neighbors() {
204 let g = make_ethanol();
205 let c2 = NodeIndex::new(1);
206 let neighbors = g.heavy_neighbors(c2);
207 assert_eq!(neighbors.len(), 2);
208 }
209}