1use super::*;
2
3pub struct ChemistryModelingLibrary {
5 molecular_simulator: MolecularSimulator,
6 quantum_calculator: QuantumCalculator,
7 reaction_analyzer: ReactionAnalyzer,
8 property_predictor: PropertyPredictor,
9 performance_monitor: ChemistryPerformanceMonitor,
10 linear_algebra: Option<Arc<Mutex<LinearAlgebraLibrary>>>,
16 statistical_computing: Option<Arc<Mutex<StatisticalComputingLibrary>>>,
17 csd_manager: Option<Arc<Mutex<CsdManager>>>,
18 zns_manager: Option<Arc<Mutex<ZnsZoneManager>>>,
19}
20
21impl ChemistryModelingLibrary {
22 pub fn new() -> Self {
24 Self {
25 molecular_simulator: MolecularSimulator::new(),
26 quantum_calculator: QuantumCalculator::new(),
27 reaction_analyzer: ReactionAnalyzer::new(),
28 property_predictor: PropertyPredictor::new(),
29 performance_monitor: ChemistryPerformanceMonitor::new(),
30 linear_algebra: None,
32 statistical_computing: None,
33 csd_manager: None,
34 zns_manager: None,
35 }
36 }
37
38 pub fn attach_dependencies(
43 &mut self,
44 linear_algebra: Arc<Mutex<LinearAlgebraLibrary>>,
45 statistical_computing: Arc<Mutex<StatisticalComputingLibrary>>,
46 csd_manager: Arc<Mutex<CsdManager>>,
47 zns_manager: Arc<Mutex<ZnsZoneManager>>,
48 ) {
49 self.linear_algebra = Some(linear_algebra.clone());
50 self.statistical_computing = Some(statistical_computing.clone());
51 self.csd_manager = Some(csd_manager);
52 self.zns_manager = Some(zns_manager);
53 self.molecular_simulator.attach_dependencies(
54 self.linear_algebra.clone(),
55 self.statistical_computing.clone(),
56 );
57 }
58
59 pub fn initialize(&mut self) -> Result<(), ChemistryError> {
61 self.molecular_simulator.initialize()?;
68
69 self.quantum_calculator.initialize()?;
71
72 self.reaction_analyzer.initialize()?;
74
75 self.property_predictor.initialize()?;
77
78 Ok(())
79 }
80
81 pub fn run_molecular_dynamics(
83 &mut self,
84 config: SimulationConfig,
85 molecule: Molecule,
86 ) -> Result<ChemistryOperationResult<SimulationTrajectory>, ChemistryError> {
87 let start_time = std::time::Instant::now();
88
89 self.molecular_simulator.validate_config(&config)?;
91
92 self.molecular_simulator.store_molecule(molecule.clone());
94
95 let trajectory = self
97 .molecular_simulator
98 .run_simulation(&config, &molecule)?;
99
100 let execution_time = start_time.elapsed().as_millis() as u64;
101
102 let drift = trajectory.properties.energy_drift;
107 let iterations = trajectory.properties.total_frames as u32;
108 Ok(ChemistryOperationResult {
109 result: trajectory,
110 execution_time,
111 computational_cost: 0.0,
112 accuracy: 0.0, convergence_info: ConvergenceInfo {
114 converged: drift < 1e-3,
117 iterations,
118 convergence_criterion: 1e-3,
119 final_error: drift,
120 },
121 })
122 }
123
124 pub fn calculate_quantum_properties(
126 &mut self,
127 molecule: Molecule,
128 method: QuantumMethodType,
129 ) -> Result<ChemistryOperationResult<QuantumProperties>, ChemistryError> {
130 let start_time = std::time::Instant::now();
131
132 self.quantum_calculator.validate_molecule(&molecule)?;
134
135 let properties = self
137 .quantum_calculator
138 .calculate_properties(&molecule, method)?;
139
140 let execution_time = start_time.elapsed().as_millis() as u64;
141
142 Ok(ChemistryOperationResult {
143 result: properties,
144 execution_time,
145 computational_cost: 0.0,
146 accuracy: 0.0, convergence_info: ConvergenceInfo {
148 converged: true,
149 iterations: 50,
150 convergence_criterion: 1e-8,
151 final_error: 1e-10,
152 },
153 })
154 }
155
156 pub fn analyze_reaction_kinetics(
158 &mut self,
159 reaction: Reaction,
160 conditions: ReactionConditions,
161 ) -> Result<ChemistryOperationResult<KineticsResults>, ChemistryError> {
162 let start_time = std::time::Instant::now();
163
164 self.reaction_analyzer.validate_reaction(&reaction)?;
166
167 let results = self
169 .reaction_analyzer
170 .analyze_kinetics(&reaction, &conditions)?;
171
172 let execution_time = start_time.elapsed().as_millis() as u64;
173
174 Ok(ChemistryOperationResult {
175 result: results,
176 execution_time,
177 computational_cost: 0.0,
178 accuracy: 0.0, convergence_info: ConvergenceInfo {
181 converged: true,
182 iterations: 1,
183 convergence_criterion: 0.0,
184 final_error: 0.0,
185 },
186 })
187 }
188
189 pub fn predict_properties(
191 &mut self,
192 molecule: Molecule,
193 properties: Vec<PropertyType>,
194 ) -> Result<ChemistryOperationResult<PredictedProperties>, ChemistryError> {
195 let start_time = std::time::Instant::now();
196
197 self.property_predictor.validate_molecule(&molecule)?;
199
200 let predicted = self
202 .property_predictor
203 .predict_from_molecule(&molecule, &properties)?;
204
205 let execution_time = start_time.elapsed().as_millis() as u64;
206
207 Ok(ChemistryOperationResult {
208 result: predicted,
209 execution_time,
210 computational_cost: 0.0,
211 accuracy: 0.0, convergence_info: ConvergenceInfo {
213 converged: true,
214 iterations: 10,
215 convergence_criterion: 1e-4,
216 final_error: 1e-5,
217 },
218 })
219 }
220
221 pub fn get_performance_stats(&self) -> ChemistryPerformanceMetrics {
223 self.performance_monitor.get_metrics()
224 }
225
226 pub fn list_force_fields(&self) -> Vec<String> {
228 self.molecular_simulator.list_force_fields()
229 }
230
231 pub fn get_molecule_info(&self, molecule_id: &str) -> Option<Molecule> {
233 self.molecular_simulator.get_molecule(molecule_id)
234 }
235
236 pub fn molecular_mass(&self, molecule: &Molecule) -> f64 {
250 molecule
251 .atoms
252 .iter()
253 .map(|a| standard_atomic_weight(&a.element).unwrap_or(a.mass))
254 .sum()
255 }
256
257 pub fn molecular_formula(&self, molecule: &Molecule) -> String {
261 use std::collections::BTreeMap;
262 let mut counts: BTreeMap<String, usize> = BTreeMap::new();
263 for a in &molecule.atoms {
264 *counts.entry(a.element.clone()).or_insert(0) += 1;
265 }
266 let mut out = String::new();
267 let mut push = |el: &str, n: usize| {
268 out.push_str(el);
269 if n > 1 {
270 out.push_str(&n.to_string());
271 }
272 };
273 if let Some(&c) = counts.get("C") {
275 push("C", c);
276 counts.remove("C");
277 if let Some(&h) = counts.get("H") {
278 push("H", h);
279 counts.remove("H");
280 }
281 }
282 for (el, n) in &counts {
284 push(el, *n);
285 }
286 out
287 }
288
289 pub fn nuclear_repulsion_energy(&self, molecule: &Molecule) -> Result<f64, ChemistryError> {
297 let atoms = &molecule.atoms;
298 for (i, a) in atoms.iter().enumerate() {
299 if a.coordinates.len() != 3 {
300 return Err(ChemistryError::InsufficientData(format!(
301 "nuclear repulsion: atom {} ('{}') has {} coordinates; 3 are required",
302 i,
303 a.atom_id,
304 a.coordinates.len()
305 )));
306 }
307 if a.atomic_number == 0 {
308 return Err(ChemistryError::InsufficientData(format!(
309 "nuclear repulsion: atom {} ('{}', element '{}') has atomic number 0; \
310 a nuclear charge is required — refusing to invent one",
311 i, a.atom_id, a.element
312 )));
313 }
314 }
315 let mut e = 0.0;
316 for i in 0..atoms.len() {
317 for j in (i + 1)..atoms.len() {
318 let ci = &atoms[i].coordinates;
319 let cj = &atoms[j].coordinates;
320 let dx = ci[0] - cj[0];
321 let dy = ci[1] - cj[1];
322 let dz = ci[2] - cj[2];
323 let r = (dx * dx + dy * dy + dz * dz).sqrt();
324 if r <= 0.0 {
325 return Err(ChemistryError::ValidationError(format!(
326 "nuclear repulsion: atoms {} and {} are coincident (r = 0); \
327 the Coulomb term is singular",
328 i, j
329 )));
330 }
331 e += (atoms[i].atomic_number as f64) * (atoms[j].atomic_number as f64) / r;
332 }
333 }
334 Ok(e)
335 }
336
337 pub fn bond_length(
340 &self,
341 molecule: &Molecule,
342 i: usize,
343 j: usize,
344 ) -> Result<f64, ChemistryError> {
345 let a = atom_coords(molecule, i)?;
346 let b = atom_coords(molecule, j)?;
347 let dx = a[0] - b[0];
348 let dy = a[1] - b[1];
349 let dz = a[2] - b[2];
350 Ok((dx * dx + dy * dy + dz * dz).sqrt())
351 }
352
353 pub fn bond_angle(
356 &self,
357 molecule: &Molecule,
358 i: usize,
359 j: usize,
360 k: usize,
361 ) -> Result<f64, ChemistryError> {
362 let ri = atom_coords(molecule, i)?;
363 let rj = atom_coords(molecule, j)?;
364 let rk = atom_coords(molecule, k)?;
365 let u = [ri[0] - rj[0], ri[1] - rj[1], ri[2] - rj[2]];
366 let v = [rk[0] - rj[0], rk[1] - rj[1], rk[2] - rj[2]];
367 let nu = (u[0] * u[0] + u[1] * u[1] + u[2] * u[2]).sqrt();
368 let nv = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
369 if nu <= 0.0 || nv <= 0.0 {
370 return Err(ChemistryError::ValidationError(
371 "bond angle: a bonding vector has zero length (coincident atoms)".to_string(),
372 ));
373 }
374 let cos = ((u[0] * v[0] + u[1] * v[1] + u[2] * v[2]) / (nu * nv)).clamp(-1.0, 1.0);
375 Ok(cos.acos())
376 }
377
378 pub fn center_of_mass(&self, molecule: &Molecule) -> Result<[f64; 3], ChemistryError> {
381 if molecule.atoms.is_empty() {
382 return Err(ChemistryError::InsufficientData(
383 "center of mass: the molecule has no atoms".to_string(),
384 ));
385 }
386 let mut m_total = 0.0;
387 let mut com = [0.0; 3];
388 for (idx, a) in molecule.atoms.iter().enumerate() {
389 let c = atom_coords(molecule, idx)?;
390 let m = standard_atomic_weight(&a.element).unwrap_or(a.mass);
391 m_total += m;
392 for d in 0..3 {
393 com[d] += m * c[d];
394 }
395 }
396 if m_total <= 0.0 {
397 return Err(ChemistryError::InsufficientData(
398 "center of mass: total mass is non-positive".to_string(),
399 ));
400 }
401 for d in 0..3 {
402 com[d] /= m_total;
403 }
404 Ok(com)
405 }
406
407 pub fn principal_moments_of_inertia(
411 &self,
412 molecule: &Molecule,
413 ) -> Result<[f64; 3], ChemistryError> {
414 let com = self.center_of_mass(molecule)?;
415 let mut tensor =
416 crate::specialized_libs::shared::zero_heap_algebra::ZeroHeapMatrix::<f64, 3, 3>::zeros(
417 );
418 let mut ixx = 0.0;
419 let mut iyy = 0.0;
420 let mut izz = 0.0;
421 let mut ixy = 0.0;
422 let mut ixz = 0.0;
423 let mut iyz = 0.0;
424 for (idx, a) in molecule.atoms.iter().enumerate() {
425 let c = atom_coords(molecule, idx)?;
426 let m = standard_atomic_weight(&a.element).unwrap_or(a.mass);
427 let x = c[0] - com[0];
428 let y = c[1] - com[1];
429 let z = c[2] - com[2];
430 ixx += m * (y * y + z * z);
431 iyy += m * (x * x + z * z);
432 izz += m * (x * x + y * y);
433 ixy -= m * x * y;
434 ixz -= m * x * z;
435 iyz -= m * y * z;
436 }
437 tensor.set(0, 0, ixx);
438 tensor.set(1, 1, iyy);
439 tensor.set(2, 2, izz);
440 tensor.set(0, 1, ixy);
441 tensor.set(1, 0, ixy);
442 tensor.set(0, 2, ixz);
443 tensor.set(2, 0, ixz);
444 tensor.set(1, 2, iyz);
445 tensor.set(2, 1, iyz);
446 let (evals, _) = scf::jacobi_diagonalization(&tensor).map_err(|_| {
447 ChemistryError::ConvergenceError(
448 "principal moments of inertia: inertia-tensor diagonalization did not converge"
449 .to_string(),
450 )
451 })?;
452 Ok([evals[0], evals[1], evals[2]])
454 }
455
456 pub fn structural_properties(
461 &self,
462 molecule: &Molecule,
463 ) -> Result<StructuralProperties, ChemistryError> {
464 if molecule.atoms.is_empty() {
465 return Err(ChemistryError::InsufficientData(
466 "structural properties: the molecule has no atoms".to_string(),
467 ));
468 }
469 Ok(StructuralProperties {
470 molecular_mass: self.molecular_mass(molecule),
471 formula: self.molecular_formula(molecule),
472 atom_count: molecule.atoms.len(),
473 nuclear_repulsion_energy: self.nuclear_repulsion_energy(molecule).ok(),
474 center_of_mass: self.center_of_mass(molecule)?,
475 principal_moments_of_inertia: self.principal_moments_of_inertia(molecule)?,
476 })
477 }
478}
479
480fn atom_coords(molecule: &Molecule, i: usize) -> Result<[f64; 3], ChemistryError> {
483 let a = molecule.atoms.get(i).ok_or_else(|| {
484 ChemistryError::ValidationError(format!(
485 "atom index {} out of range ({} atoms)",
486 i,
487 molecule.atoms.len()
488 ))
489 })?;
490 if a.coordinates.len() != 3 {
491 return Err(ChemistryError::InsufficientData(format!(
492 "atom {} ('{}') has {} coordinates; 3 are required",
493 i,
494 a.atom_id,
495 a.coordinates.len()
496 )));
497 }
498 Ok([a.coordinates[0], a.coordinates[1], a.coordinates[2]])
499}