Skip to main content

sci_form/
lib.rs

1// Scientific/numerical code patterns that are idiomatic in this domain
2#![allow(clippy::too_many_arguments)]
3#![allow(clippy::needless_range_loop)]
4
5pub mod conformer;
6pub mod distgeom;
7pub mod etkdg;
8pub mod forcefield;
9pub mod graph;
10pub mod optimization;
11pub mod smarts;
12pub mod smiles;
13
14use serde::{Deserialize, Serialize};
15
16// ─── Public API Types ────────────────────────────────────────────────────────
17
18/// Result of a 3D conformer generation for a single molecule.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ConformerResult {
21    /// Input SMILES string.
22    pub smiles: String,
23    /// Number of atoms in the molecule.
24    pub num_atoms: usize,
25    /// Flat xyz coordinates: [x0, y0, z0, x1, y1, z1, ...].
26    /// Empty on failure.
27    pub coords: Vec<f64>,
28    /// Atom elements (atomic numbers) in the same order as coords.
29    pub elements: Vec<u8>,
30    /// Bond list as (start_atom, end_atom, order_string).
31    pub bonds: Vec<(usize, usize, String)>,
32    /// Error message if generation failed.
33    pub error: Option<String>,
34    /// Generation time in milliseconds.
35    pub time_ms: f64,
36}
37
38/// Configuration for conformer generation.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ConformerConfig {
41    /// RNG seed (same seed = reproducible output).
42    pub seed: u64,
43    /// Number of threads for batch processing (0 = auto-detect).
44    pub num_threads: usize,
45}
46
47impl Default for ConformerConfig {
48    fn default() -> Self {
49        Self {
50            seed: 42,
51            num_threads: 0,
52        }
53    }
54}
55
56// ─── Public API Functions ────────────────────────────────────────────────────
57
58/// Library version string.
59pub fn version() -> String {
60    format!("sci-form {}", env!("CARGO_PKG_VERSION"))
61}
62
63/// Generate a 3D conformer from a SMILES string.
64pub fn embed(smiles: &str, seed: u64) -> ConformerResult {
65    #[cfg(not(target_arch = "wasm32"))]
66    let start = std::time::Instant::now();
67
68    let mol = match graph::Molecule::from_smiles(smiles) {
69        Ok(m) => m,
70        Err(e) => {
71            return ConformerResult {
72                smiles: smiles.to_string(),
73                num_atoms: 0,
74                coords: vec![],
75                elements: vec![],
76                bonds: vec![],
77                error: Some(e),
78                #[cfg(not(target_arch = "wasm32"))]
79                time_ms: start.elapsed().as_secs_f64() * 1000.0,
80                #[cfg(target_arch = "wasm32")]
81                time_ms: 0.0,
82            };
83        }
84    };
85
86    let n = mol.graph.node_count();
87    let elements: Vec<u8> = (0..n)
88        .map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
89        .collect();
90    let bonds: Vec<(usize, usize, String)> = mol
91        .graph
92        .edge_indices()
93        .map(|e| {
94            let (a, b) = mol.graph.edge_endpoints(e).unwrap();
95            let order = match mol.graph[e].order {
96                graph::BondOrder::Single => "SINGLE",
97                graph::BondOrder::Double => "DOUBLE",
98                graph::BondOrder::Triple => "TRIPLE",
99                graph::BondOrder::Aromatic => "AROMATIC",
100                graph::BondOrder::Unknown => "UNKNOWN",
101            };
102            (a.index(), b.index(), order.to_string())
103        })
104        .collect();
105
106    match conformer::generate_3d_conformer(&mol, seed) {
107        Ok(coords) => {
108            let mut flat = Vec::with_capacity(n * 3);
109            for i in 0..n {
110                flat.push(coords[(i, 0)] as f64);
111                flat.push(coords[(i, 1)] as f64);
112                flat.push(coords[(i, 2)] as f64);
113            }
114            ConformerResult {
115                smiles: smiles.to_string(),
116                num_atoms: n,
117                coords: flat,
118                elements,
119                bonds,
120                error: None,
121                #[cfg(not(target_arch = "wasm32"))]
122                time_ms: start.elapsed().as_secs_f64() * 1000.0,
123                #[cfg(target_arch = "wasm32")]
124                time_ms: 0.0,
125            }
126        }
127        Err(e) => ConformerResult {
128            smiles: smiles.to_string(),
129            num_atoms: n,
130            coords: vec![],
131            elements,
132            bonds,
133            error: Some(e),
134            #[cfg(not(target_arch = "wasm32"))]
135            time_ms: start.elapsed().as_secs_f64() * 1000.0,
136            #[cfg(target_arch = "wasm32")]
137            time_ms: 0.0,
138        },
139    }
140}
141
142/// Batch-embed multiple SMILES in parallel.
143///
144/// Uses rayon thread pool for parallel processing.
145/// `config.num_threads = 0` means auto-detect CPU count.
146#[cfg(feature = "parallel")]
147pub fn embed_batch(smiles_list: &[&str], config: &ConformerConfig) -> Vec<ConformerResult> {
148    use rayon::prelude::*;
149
150    if config.num_threads > 0 {
151        let pool = rayon::ThreadPoolBuilder::new()
152            .num_threads(config.num_threads)
153            .build()
154            .unwrap();
155        pool.install(|| {
156            smiles_list
157                .par_iter()
158                .map(|smi| embed(smi, config.seed))
159                .collect()
160        })
161    } else {
162        smiles_list
163            .par_iter()
164            .map(|smi| embed(smi, config.seed))
165            .collect()
166    }
167}
168
169/// Batch-embed multiple SMILES sequentially (no rayon dependency).
170#[cfg(not(feature = "parallel"))]
171pub fn embed_batch(smiles_list: &[&str], config: &ConformerConfig) -> Vec<ConformerResult> {
172    smiles_list
173        .iter()
174        .map(|smi| embed(smi, config.seed))
175        .collect()
176}
177
178/// Parse a SMILES string and return molecular structure (no 3D generation).
179pub fn parse(smiles: &str) -> Result<graph::Molecule, String> {
180    graph::Molecule::from_smiles(smiles)
181}