1#![allow(clippy::too_many_arguments)]
3#![allow(clippy::needless_range_loop)]
4
5pub mod alignment;
6pub mod charges;
7pub mod conformer;
8pub mod dipole;
9pub mod distgeom;
10pub mod dos;
11pub mod eht;
12pub mod esp;
13pub mod etkdg;
14pub mod forcefield;
15pub mod graph;
16pub mod materials;
17pub mod optimization;
18pub mod population;
19pub mod smarts;
20pub mod smiles;
21pub mod surface;
22pub mod transport;
23
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ConformerResult {
31 pub smiles: String,
33 pub num_atoms: usize,
35 pub coords: Vec<f64>,
38 pub elements: Vec<u8>,
40 pub bonds: Vec<(usize, usize, String)>,
42 pub error: Option<String>,
44 pub time_ms: f64,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ConformerConfig {
51 pub seed: u64,
53 pub num_threads: usize,
55}
56
57impl Default for ConformerConfig {
58 fn default() -> Self {
59 Self {
60 seed: 42,
61 num_threads: 0,
62 }
63 }
64}
65
66pub fn version() -> String {
70 format!("sci-form {}", env!("CARGO_PKG_VERSION"))
71}
72
73pub fn embed(smiles: &str, seed: u64) -> ConformerResult {
75 #[cfg(not(target_arch = "wasm32"))]
76 let start = std::time::Instant::now();
77
78 let mol = match graph::Molecule::from_smiles(smiles) {
79 Ok(m) => m,
80 Err(e) => {
81 return ConformerResult {
82 smiles: smiles.to_string(),
83 num_atoms: 0,
84 coords: vec![],
85 elements: vec![],
86 bonds: vec![],
87 error: Some(e),
88 #[cfg(not(target_arch = "wasm32"))]
89 time_ms: start.elapsed().as_secs_f64() * 1000.0,
90 #[cfg(target_arch = "wasm32")]
91 time_ms: 0.0,
92 };
93 }
94 };
95
96 let n = mol.graph.node_count();
97 let elements: Vec<u8> = (0..n)
98 .map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
99 .collect();
100 let bonds: Vec<(usize, usize, String)> = mol
101 .graph
102 .edge_indices()
103 .map(|e| {
104 let (a, b) = mol.graph.edge_endpoints(e).unwrap();
105 let order = match mol.graph[e].order {
106 graph::BondOrder::Single => "SINGLE",
107 graph::BondOrder::Double => "DOUBLE",
108 graph::BondOrder::Triple => "TRIPLE",
109 graph::BondOrder::Aromatic => "AROMATIC",
110 graph::BondOrder::Unknown => "UNKNOWN",
111 };
112 (a.index(), b.index(), order.to_string())
113 })
114 .collect();
115
116 match conformer::generate_3d_conformer(&mol, seed) {
117 Ok(coords) => {
118 let mut flat = Vec::with_capacity(n * 3);
119 for i in 0..n {
120 flat.push(coords[(i, 0)] as f64);
121 flat.push(coords[(i, 1)] as f64);
122 flat.push(coords[(i, 2)] as f64);
123 }
124 ConformerResult {
125 smiles: smiles.to_string(),
126 num_atoms: n,
127 coords: flat,
128 elements,
129 bonds,
130 error: None,
131 #[cfg(not(target_arch = "wasm32"))]
132 time_ms: start.elapsed().as_secs_f64() * 1000.0,
133 #[cfg(target_arch = "wasm32")]
134 time_ms: 0.0,
135 }
136 }
137 Err(e) => ConformerResult {
138 smiles: smiles.to_string(),
139 num_atoms: n,
140 coords: vec![],
141 elements,
142 bonds,
143 error: Some(e),
144 #[cfg(not(target_arch = "wasm32"))]
145 time_ms: start.elapsed().as_secs_f64() * 1000.0,
146 #[cfg(target_arch = "wasm32")]
147 time_ms: 0.0,
148 },
149 }
150}
151
152#[cfg(feature = "parallel")]
157pub fn embed_batch(smiles_list: &[&str], config: &ConformerConfig) -> Vec<ConformerResult> {
158 use rayon::prelude::*;
159
160 if config.num_threads > 0 {
161 let pool = rayon::ThreadPoolBuilder::new()
162 .num_threads(config.num_threads)
163 .build()
164 .unwrap();
165 pool.install(|| {
166 smiles_list
167 .par_iter()
168 .map(|smi| embed(smi, config.seed))
169 .collect()
170 })
171 } else {
172 smiles_list
173 .par_iter()
174 .map(|smi| embed(smi, config.seed))
175 .collect()
176 }
177}
178
179#[cfg(not(feature = "parallel"))]
181pub fn embed_batch(smiles_list: &[&str], config: &ConformerConfig) -> Vec<ConformerResult> {
182 smiles_list
183 .iter()
184 .map(|smi| embed(smi, config.seed))
185 .collect()
186}
187
188pub fn parse(smiles: &str) -> Result<graph::Molecule, String> {
190 graph::Molecule::from_smiles(smiles)
191}
192
193pub fn compute_charges(smiles: &str) -> Result<charges::gasteiger::ChargeResult, String> {
198 let mol = graph::Molecule::from_smiles(smiles)?;
199 let n = mol.graph.node_count();
200 let elements: Vec<u8> = (0..n)
201 .map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
202 .collect();
203 let formal_charges: Vec<i8> = (0..n)
204 .map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].formal_charge)
205 .collect();
206 let bonds: Vec<(usize, usize)> = mol
207 .graph
208 .edge_indices()
209 .map(|e| {
210 let (a, b) = mol.graph.edge_endpoints(e).unwrap();
211 (a.index(), b.index())
212 })
213 .collect();
214 charges::gasteiger::gasteiger_marsili_charges(&elements, &bonds, &formal_charges, 6)
215}
216
217pub fn compute_sasa(
221 elements: &[u8],
222 coords_flat: &[f64],
223 probe_radius: Option<f64>,
224) -> Result<surface::sasa::SasaResult, String> {
225 if coords_flat.len() != elements.len() * 3 {
226 return Err(format!(
227 "coords length {} != 3 * elements {}",
228 coords_flat.len(),
229 elements.len()
230 ));
231 }
232 let positions: Vec<[f64; 3]> = coords_flat
233 .chunks_exact(3)
234 .map(|c| [c[0], c[1], c[2]])
235 .collect();
236
237 #[cfg(feature = "parallel")]
238 {
239 Ok(surface::sasa::compute_sasa_parallel(
240 elements,
241 &positions,
242 probe_radius,
243 None,
244 ))
245 }
246
247 #[cfg(not(feature = "parallel"))]
248 {
249 Ok(surface::sasa::compute_sasa(
250 elements,
251 &positions,
252 probe_radius,
253 None,
254 ))
255 }
256}
257
258pub fn compute_population(
260 elements: &[u8],
261 positions: &[[f64; 3]],
262) -> Result<population::PopulationResult, String> {
263 let eht_result = eht::solve_eht(elements, positions, None)?;
264 Ok(population::compute_population(
265 elements,
266 positions,
267 &eht_result.coefficients,
268 eht_result.n_electrons,
269 ))
270}
271
272pub fn compute_dipole(
274 elements: &[u8],
275 positions: &[[f64; 3]],
276) -> Result<dipole::DipoleResult, String> {
277 let eht_result = eht::solve_eht(elements, positions, None)?;
278 Ok(dipole::compute_dipole_from_eht(
279 elements,
280 positions,
281 &eht_result.coefficients,
282 eht_result.n_electrons,
283 ))
284}
285
286pub fn compute_esp(
288 elements: &[u8],
289 positions: &[[f64; 3]],
290 spacing: f64,
291 padding: f64,
292) -> Result<esp::EspGrid, String> {
293 let pop = compute_population(elements, positions)?;
294 #[cfg(feature = "parallel")]
295 {
296 Ok(esp::compute_esp_grid_parallel(
297 elements,
298 positions,
299 &pop.mulliken_charges,
300 spacing,
301 padding,
302 ))
303 }
304
305 #[cfg(not(feature = "parallel"))]
306 {
307 Ok(esp::compute_esp_grid(
308 elements,
309 positions,
310 &pop.mulliken_charges,
311 spacing,
312 padding,
313 ))
314 }
315}
316
317pub fn compute_dos(
319 elements: &[u8],
320 positions: &[[f64; 3]],
321 sigma: f64,
322 e_min: f64,
323 e_max: f64,
324 n_points: usize,
325) -> Result<dos::DosResult, String> {
326 let eht_result = eht::solve_eht(elements, positions, None)?;
327 let flat: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
328
329 #[cfg(feature = "parallel")]
330 {
331 Ok(dos::compute_pdos_parallel(
332 elements,
333 &flat,
334 &eht_result.energies,
335 &eht_result.coefficients,
336 eht_result.n_electrons,
337 sigma,
338 e_min,
339 e_max,
340 n_points,
341 ))
342 }
343
344 #[cfg(not(feature = "parallel"))]
345 {
346 Ok(dos::compute_pdos(
347 elements,
348 &flat,
349 &eht_result.energies,
350 &eht_result.coefficients,
351 eht_result.n_electrons,
352 sigma,
353 e_min,
354 e_max,
355 n_points,
356 ))
357 }
358}
359
360pub fn compute_rmsd(coords: &[f64], reference: &[f64]) -> f64 {
362 alignment::compute_rmsd(coords, reference)
363}
364
365pub fn compute_uff_energy(smiles: &str, coords: &[f64]) -> Result<f64, String> {
367 let mol = graph::Molecule::from_smiles(smiles)?;
368 let n = mol.graph.node_count();
369 if coords.len() != n * 3 {
370 return Err(format!("coords length {} != 3 * atoms {}", coords.len(), n));
371 }
372 let ff = forcefield::builder::build_uff_force_field(&mol);
373 let mut gradient = vec![0.0f64; n * 3];
374 let energy = ff.compute_system_energy_and_gradients(coords, &mut gradient);
375 Ok(energy)
376}
377
378pub fn create_unit_cell(
380 a: f64,
381 b: f64,
382 c: f64,
383 alpha: f64,
384 beta: f64,
385 gamma: f64,
386) -> materials::UnitCell {
387 materials::UnitCell::from_parameters(&materials::CellParameters {
388 a,
389 b,
390 c,
391 alpha,
392 beta,
393 gamma,
394 })
395}
396
397pub fn assemble_framework(
401 node: &materials::Sbu,
402 linker: &materials::Sbu,
403 topology: &materials::Topology,
404 cell: &materials::UnitCell,
405) -> materials::CrystalStructure {
406 materials::assemble_framework(node, linker, topology, cell)
407}