1#![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#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ConformerResult {
21 pub smiles: String,
23 pub num_atoms: usize,
25 pub coords: Vec<f64>,
28 pub elements: Vec<u8>,
30 pub bonds: Vec<(usize, usize, String)>,
32 pub error: Option<String>,
34 pub time_ms: f64,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ConformerConfig {
41 pub seed: u64,
43 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
56pub fn version() -> String {
60 format!("sci-form {}", env!("CARGO_PKG_VERSION"))
61}
62
63pub 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#[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#[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
178pub fn parse(smiles: &str) -> Result<graph::Molecule, String> {
180 graph::Molecule::from_smiles(smiles)
181}