Skip to main content

chematic_rxn/
enumerate.rs

1//! Combinatorial library enumeration from SMIRKS templates and fragment sets.
2//!
3//! Generates virtual libraries by combining reaction templates with scaffolds and building blocks.
4
5use crate::transform::run_reactants;
6use chematic_core::Molecule;
7
8/// Configuration for library enumeration.
9#[derive(Clone, Debug)]
10pub struct LibraryConfig {
11    /// Skip molecules that fail transformation (vs collecting errors).
12    pub skip_failures: bool,
13    /// Maximum total library size to prevent runaway enumeration.
14    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
26/// Enumerate a combinatorial library from reaction templates and fragment sets.
27///
28/// Given N SMIRKS templates and M fragment sets, generates all valid products
29/// where each template is applied with one fragment from each set.
30///
31/// # Arguments
32/// * `templates` - SMIRKS reaction templates (e.g., `"[C:1][Cl].[C:2][NH2]>>[C:1][NH][C:2]"`)
33/// * `fragment_sets` - One or more sets of fragments (e.g., `vec![vec![scaffold1, scaffold2], vec![sidechain1, sidechain2]]`)
34/// * `config` - Enumeration options (max size, skip failures)
35///
36/// # Returns
37/// `Vec<Molecule>` containing all enumerated products (may be deduplicated by hash).
38///
39/// # Example
40/// ```ignore
41/// let template = "[C:1]Cl.[C:2]N>>[C:1]N[C:2]"; // Amide coupling
42/// let scaffolds = vec![phenol, aniline];
43/// let sidechains = vec![acetyl, benzoyl];
44/// let library = enumerate_library(
45///     &template,
46///     vec![scaffolds, sidechains],
47///     &LibraryConfig::default()
48/// );
49/// // Returns all products: phenol + acetyl, phenol + benzoyl, aniline + acetyl, aniline + benzoyl
50/// ```
51pub 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    // Check max size feasibility
61    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        // Build reactants list for this iteration (as references)
74        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        // Apply template
80        match run_reactants(template, &reactants) {
81            Ok(reaction_product_sets) => {
82                // Flatten: each set can have multiple products
83                for product_set in reaction_product_sets {
84                    products.extend(product_set);
85                }
86            }
87            Err(_) => {
88                if !config.skip_failures {
89                    // For now, skip and continue (could collect errors)
90                }
91            }
92        }
93
94        // Check product count (actual size, not just iteration count)
95        if let Some(max) = config.max_size
96            && products.len() >= max
97        {
98            return Err(LibraryError::EnumerationLimitExceeded);
99        }
100
101        // Increment indices (counter-like behavior)
102        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 is 1 after loop, we've exhausted all combinations
113        if carry == 1 {
114            break;
115        }
116    }
117
118    Ok(products)
119}
120
121/// Simpler API: enumerate with a single template and two fragment sets (common case).
122///
123/// Useful for reactions like: `scaffolds × building_blocks → products`
124pub 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
133/// Enumerate with three fragment sets (also common: scaffold × R1 × R2 → products).
134pub 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/// Errors from library enumeration.
145#[derive(Debug, Clone)]
146pub enum LibraryError {
147    /// No fragment sets provided.
148    NoFragmentSets,
149    /// Total combinations exceeds configured maximum.
150    EnumerationTooLarge(usize, usize), // actual, max
151    /// Hit iteration limit (safety check).
152    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        // Simple amide coupling: phenol + amine → amide
181        let template = "[C:1][OH].[C:2][NH2]>>[C:1]O[C:2]"; // Ether formation
182        let scaffolds = vec![mol("c1ccccc1O"), mol("Cc1ccccc1O")]; // Two phenols
183        let building_blocks = vec![mol("c1ccccc1N"), mol("CCN")]; // Two amines
184
185        let result = enumerate_library_2way(
186            template,
187            scaffolds,
188            building_blocks,
189            &LibraryConfig::default(),
190        );
191
192        match result {
193            Ok(products) => {
194                // Should have at most 2×2=4 products
195                assert!(
196                    products.len() <= 4,
197                    "expected <= 4 products, got {}",
198                    products.len()
199                );
200            }
201            Err(_) => {
202                // If template fails, that's OK for this test (focus on enumeration logic)
203            }
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        // Create a config that rejects large enumerations
217        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        // 4×4 = 16 > 3, should fail
228        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]"; // Dummy template
237        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        // Should enumerate 2×1×1 = 2 combinations
253        match result {
254            Ok(products) => {
255                assert!(products.len() <= 2, "expected <= 2 products");
256            }
257            Err(_) => {
258                // Template might fail, that's OK
259            }
260        }
261    }
262}