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 Ok(surface::sasa::compute_sasa(
237 elements,
238 &positions,
239 probe_radius,
240 None,
241 ))
242}
243
244pub fn compute_population(
246 elements: &[u8],
247 positions: &[[f64; 3]],
248) -> Result<population::PopulationResult, String> {
249 let eht_result = eht::solve_eht(elements, positions, None)?;
250 Ok(population::compute_population(
251 elements,
252 positions,
253 &eht_result.coefficients,
254 eht_result.n_electrons,
255 ))
256}
257
258pub fn compute_dipole(
260 elements: &[u8],
261 positions: &[[f64; 3]],
262) -> Result<dipole::DipoleResult, String> {
263 let eht_result = eht::solve_eht(elements, positions, None)?;
264 Ok(dipole::compute_dipole_from_eht(
265 elements,
266 positions,
267 &eht_result.coefficients,
268 eht_result.n_electrons,
269 ))
270}
271
272pub fn compute_esp(
274 elements: &[u8],
275 positions: &[[f64; 3]],
276 spacing: f64,
277 padding: f64,
278) -> Result<esp::EspGrid, String> {
279 let pop = compute_population(elements, positions)?;
280 Ok(esp::compute_esp_grid(
281 elements,
282 positions,
283 &pop.mulliken_charges,
284 spacing,
285 padding,
286 ))
287}
288
289pub fn compute_dos(
291 elements: &[u8],
292 positions: &[[f64; 3]],
293 sigma: f64,
294 e_min: f64,
295 e_max: f64,
296 n_points: usize,
297) -> Result<dos::DosResult, String> {
298 let eht_result = eht::solve_eht(elements, positions, None)?;
299 let flat: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
300 Ok(dos::compute_pdos(
301 elements,
302 &flat,
303 &eht_result.energies,
304 &eht_result.coefficients,
305 eht_result.n_electrons,
306 sigma,
307 e_min,
308 e_max,
309 n_points,
310 ))
311}
312
313pub fn compute_rmsd(coords: &[f64], reference: &[f64]) -> f64 {
315 alignment::compute_rmsd(coords, reference)
316}
317
318pub fn compute_uff_energy(smiles: &str, coords: &[f64]) -> Result<f64, String> {
320 let mol = graph::Molecule::from_smiles(smiles)?;
321 let n = mol.graph.node_count();
322 if coords.len() != n * 3 {
323 return Err(format!("coords length {} != 3 * atoms {}", coords.len(), n));
324 }
325 let ff = forcefield::builder::build_uff_force_field(&mol);
326 let mut gradient = vec![0.0f64; n * 3];
327 let energy = ff.compute_system_energy_and_gradients(coords, &mut gradient);
328 Ok(energy)
329}
330
331pub fn create_unit_cell(
333 a: f64,
334 b: f64,
335 c: f64,
336 alpha: f64,
337 beta: f64,
338 gamma: f64,
339) -> materials::UnitCell {
340 materials::UnitCell::from_parameters(&materials::CellParameters {
341 a,
342 b,
343 c,
344 alpha,
345 beta,
346 gamma,
347 })
348}
349
350pub fn assemble_framework(
354 node: &materials::Sbu,
355 linker: &materials::Sbu,
356 topology: &materials::Topology,
357 cell: &materials::UnitCell,
358) -> materials::CrystalStructure {
359 materials::assemble_framework(node, linker, topology, cell)
360}