chematic_rxn/
enumerate.rs1use crate::transform::run_reactants;
6use chematic_core::Molecule;
7
8#[derive(Clone, Debug)]
10pub struct LibraryConfig {
11 pub skip_failures: bool,
13 pub max_size: Option<usize>,
15}
16
17impl Default for LibraryConfig {
18 fn default() -> Self {
19 Self {
20 skip_failures: true,
21 max_size: Some(1_000_000),
22 }
23 }
24}
25
26pub fn enumerate_library(
52 template: &str,
53 fragment_sets: Vec<Vec<Molecule>>,
54 config: &LibraryConfig,
55) -> Result<Vec<Molecule>, LibraryError> {
56 if fragment_sets.is_empty() {
57 return Err(LibraryError::NoFragmentSets);
58 }
59
60 let total_combos: usize = fragment_sets.iter().map(|set| set.len()).product();
62
63 if let Some(max) = config.max_size
64 && total_combos > max
65 {
66 return Err(LibraryError::EnumerationTooLarge(total_combos, max));
67 }
68
69 let mut products: Vec<Molecule> = Vec::new();
70 let mut indices = vec![0usize; fragment_sets.len()];
71
72 loop {
73 let mut reactants = Vec::new();
75 for (set_idx, frag_set) in fragment_sets.iter().enumerate() {
76 reactants.push(&frag_set[indices[set_idx]]);
77 }
78
79 match run_reactants(template, &reactants) {
81 Ok(reaction_product_sets) => {
82 for product_set in reaction_product_sets {
84 products.extend(product_set);
85 }
86 }
87 Err(_) => {
88 if !config.skip_failures {
89 }
91 }
92 }
93
94 if let Some(max) = config.max_size
96 && products.len() >= max
97 {
98 return Err(LibraryError::EnumerationLimitExceeded);
99 }
100
101 let mut carry = 1usize;
103 for i in 0..indices.len() {
104 indices[i] += carry;
105 if indices[i] < fragment_sets[i].len() {
106 carry = 0;
107 break;
108 }
109 indices[i] = 0;
110 }
111
112 if carry == 1 {
114 break;
115 }
116 }
117
118 Ok(products)
119}
120
121pub fn enumerate_library_2way(
125 template: &str,
126 scaffolds: Vec<Molecule>,
127 building_blocks: Vec<Molecule>,
128 config: &LibraryConfig,
129) -> Result<Vec<Molecule>, LibraryError> {
130 enumerate_library(template, vec![scaffolds, building_blocks], config)
131}
132
133pub fn enumerate_library_3way(
135 template: &str,
136 scaffolds: Vec<Molecule>,
137 r1_set: Vec<Molecule>,
138 r2_set: Vec<Molecule>,
139 config: &LibraryConfig,
140) -> Result<Vec<Molecule>, LibraryError> {
141 enumerate_library(template, vec![scaffolds, r1_set, r2_set], config)
142}
143
144#[derive(Debug, Clone)]
146pub enum LibraryError {
147 NoFragmentSets,
149 EnumerationTooLarge(usize, usize), EnumerationLimitExceeded,
153}
154
155impl std::fmt::Display for LibraryError {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::NoFragmentSets => write!(f, "no fragment sets provided"),
159 Self::EnumerationTooLarge(actual, max) => {
160 write!(f, "enumeration too large: {} > {}", actual, max)
161 }
162 Self::EnumerationLimitExceeded => write!(f, "enumeration iteration limit exceeded"),
163 }
164 }
165}
166
167impl std::error::Error for LibraryError {}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use chematic_smiles::parse;
173
174 fn mol(smiles: &str) -> Molecule {
175 parse(smiles).unwrap_or_else(|e| panic!("failed to parse {smiles:?}: {e}"))
176 }
177
178 #[test]
179 fn test_enumerate_library_2way_simple() {
180 let template = "[C:1][OH].[C:2][NH2]>>[C:1]O[C:2]"; let scaffolds = vec![mol("c1ccccc1O"), mol("Cc1ccccc1O")]; let building_blocks = vec![mol("c1ccccc1N"), mol("CCN")]; let result = enumerate_library_2way(
186 template,
187 scaffolds,
188 building_blocks,
189 &LibraryConfig::default(),
190 );
191
192 match result {
193 Ok(products) => {
194 assert!(
196 products.len() <= 4,
197 "expected <= 4 products, got {}",
198 products.len()
199 );
200 }
201 Err(_) => {
202 }
204 }
205 }
206
207 #[test]
208 fn test_enumerate_library_no_fragments() {
209 let template = "[C:1][Cl]>>[C:1]I";
210 let result = enumerate_library(template, vec![], &LibraryConfig::default());
211 assert!(matches!(result, Err(LibraryError::NoFragmentSets)));
212 }
213
214 #[test]
215 fn test_enumerate_library_size_check() {
216 let config = LibraryConfig {
218 skip_failures: true,
219 max_size: Some(3),
220 };
221
222 let template = "[C:1][Cl]>>[C:1]I";
223 let set1 = vec![mol("C"), mol("CC"), mol("CCC"), mol("CCCC")];
224 let set2 = vec![mol("C"), mol("CC"), mol("CCC"), mol("CCCC")];
225
226 let result = enumerate_library(template, vec![set1, set2], &config);
227 assert!(matches!(
229 result,
230 Err(LibraryError::EnumerationTooLarge(16, 3))
231 ));
232 }
233
234 #[test]
235 fn test_enumerate_library_3way() {
236 let template = "[C:1][C:2][C:3]"; let set1 = vec![mol("C"), mol("CC")];
238 let set2 = vec![mol("CCC")];
239 let set3 = vec![mol("CCCC")];
240
241 let result = enumerate_library_3way(
242 template,
243 set1,
244 set2,
245 set3,
246 &LibraryConfig {
247 skip_failures: true,
248 max_size: Some(100),
249 },
250 );
251
252 match result {
254 Ok(products) => {
255 assert!(products.len() <= 2, "expected <= 2 products");
256 }
257 Err(_) => {
258 }
260 }
261 }
262}