Skip to main content

qualia_core_db/specialized_libs/chemistry_modeling/
library.rs

1use super::*;
2
3/// Chemistry Modeling Library Manager
4pub struct ChemistryModelingLibrary {
5    molecular_simulator: MolecularSimulator,
6    quantum_calculator: QuantumCalculator,
7    reaction_analyzer: ReactionAnalyzer,
8    property_predictor: PropertyPredictor,
9    performance_monitor: ChemistryPerformanceMonitor,
10    /// Phase 2 cross-library dependencies. These are wired in via
11    /// [`attach_dependencies`](Self::attach_dependencies) after construction so
12    /// that [`new`](Self::new) stays zero-argument (callers that don't have the
13    /// hardware/linear-algebra handles yet can still create the library). When
14    /// `None`, sub-components fall back to their built-in scalar paths.
15    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    /// Create new chemistry modeling library
23    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            // Phase 2 dependencies start unset; wire them via `attach_dependencies`.
31            linear_algebra: None,
32            statistical_computing: None,
33            csd_manager: None,
34            zns_manager: None,
35        }
36    }
37
38    /// Attach the Phase 2 cross-library dependencies (linear algebra, statistical
39    /// computing, CSD computational storage, ZNS zero-copy storage). This is the
40    /// wiring point called after [`new`](Self::new) once the caller has constructed
41    /// the shared library handles. Sub-components read them through this library.
42    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    /// Initialize the library
60    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
61        // Initialize molecular simulator. When Phase 2 dependencies have been
62        // attached, they are available to sub-components via the handles stored on
63        // this library (e.g. the force-field calculator can delegate heavy linear
64        // algebra to `linear_algebra`, and trajectory analysis to
65        // `statistical_computing`); when unset, sub-components use their built-in
66        // scalar fallbacks, so initialization never fails for lack of hardware.
67        self.molecular_simulator.initialize()?;
68
69        // Initialize quantum calculator
70        self.quantum_calculator.initialize()?;
71
72        // Initialize reaction analyzer
73        self.reaction_analyzer.initialize()?;
74
75        // Initialize property predictor
76        self.property_predictor.initialize()?;
77
78        Ok(())
79    }
80
81    /// Run molecular dynamics simulation
82    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        // Validate configuration
90        self.molecular_simulator.validate_config(&config)?;
91
92        // Store molecule for retrieval
93        self.molecular_simulator.store_molecule(molecule.clone());
94
95        // Run simulation
96        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        // Real convergence info derived from the trajectory: an MD run does not
103        // "converge" iteratively, so we report the integrator's energy-drift as
104        // the quality metric (a good symplectic run keeps it small) rather than a
105        // fabricated constant.
106        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, // not measured against experiment (no validation corpus)
113            convergence_info: ConvergenceInfo {
114                // Energy is "conserved" (the meaningful MD criterion) when the
115                // peak-to-peak drift stays under 1e-3 of the mean total energy.
116                converged: drift < 1e-3,
117                iterations,
118                convergence_criterion: 1e-3,
119                final_error: drift,
120            },
121        })
122    }
123
124    /// Calculate quantum properties
125    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        // Validate molecule
133        self.quantum_calculator.validate_molecule(&molecule)?;
134
135        // Calculate quantum properties
136        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, // not measured (scaffold default; no validation performed)
147            convergence_info: ConvergenceInfo {
148                converged: true,
149                iterations: 50,
150                convergence_criterion: 1e-8,
151                final_error: 1e-10,
152            },
153        })
154    }
155
156    /// Analyze reaction kinetics
157    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        // Validate reaction
165        self.reaction_analyzer.validate_reaction(&reaction)?;
166
167        // Analyze kinetics
168        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, // not measured against experiment (the Arrhenius model itself is exact)
179            // Closed-form Arrhenius evaluation: exact, no iteration — report that honestly.
180            convergence_info: ConvergenceInfo {
181                converged: true,
182                iterations: 1,
183                convergence_criterion: 0.0,
184                final_error: 0.0,
185            },
186        })
187    }
188
189    /// Predict molecular properties
190    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        // Validate molecule
198        self.property_predictor.validate_molecule(&molecule)?;
199
200        // Predict properties
201        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, // not measured (scaffold default; no validation performed)
212            convergence_info: ConvergenceInfo {
213                converged: true,
214                iterations: 10,
215                convergence_criterion: 1e-4,
216                final_error: 1e-5,
217            },
218        })
219    }
220
221    /// Get performance statistics
222    pub fn get_performance_stats(&self) -> ChemistryPerformanceMetrics {
223        self.performance_monitor.get_metrics()
224    }
225
226    /// List available force fields
227    pub fn list_force_fields(&self) -> Vec<String> {
228        self.molecular_simulator.list_force_fields()
229    }
230
231    /// Get molecule information
232    pub fn get_molecule_info(&self, molecule_id: &str) -> Option<Molecule> {
233        self.molecular_simulator.get_molecule(molecule_id)
234    }
235
236    // ─── Exact structural / mass properties ────────────────────────────────
237    //
238    // These are computed directly from atomic data (standard atomic weights,
239    // nuclear charges) and the molecular geometry using their exact closed-form
240    // definitions — no electronic-structure approximation, no fitted parameters,
241    // nothing fabricated. Where a definition requires an eigen-decomposition
242    // (the inertia tensor) it reuses the tested `scf::jacobi_diagonalization`
243    // rather than re-deriving one. Each has a known-value test.
244
245    /// Total molecular mass in amu, summed from IUPAC standard atomic weights by
246    /// element (falling back to the atom's own declared `mass` when the element
247    /// is outside the built-in table). Reproducible and independent of whatever
248    /// per-atom `mass` the caller happened to set.
249    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    /// Molecular formula in Hill notation: carbon first, then hydrogen, then all
258    /// remaining elements in alphabetical order, each with its count (count of 1
259    /// omitted). E.g. water → `H2O`, methane → `CH4`, ethanol → `C2H6O`.
260    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        // Hill system: C and H lead only when carbon is present.
274        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        // Remaining elements alphabetical (BTreeMap iterates in sorted order).
283        for (el, n) in &counts {
284            push(el, *n);
285        }
286        out
287    }
288
289    /// Nuclear repulsion energy E_nn = Σ_{i<j} Z_i·Z_j / r_ij.
290    ///
291    /// This is the exact classical Coulomb repulsion between the point nuclei; it
292    /// is returned in atomic units (Hartree) when the atom `coordinates` are in
293    /// bohr. A single atom (or none) has no nuclear pairs and returns `0.0`.
294    /// Refuses (rather than inventing a value) when any atom has a zero nuclear
295    /// charge, a malformed coordinate vector, or two nuclei coincide.
296    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    /// Bond length (Euclidean distance) between atoms `i` and `j`, in the same
338    /// length unit as the coordinates.
339    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    /// Bond angle i–j–k in radians, with `j` the vertex. Computed from the exact
354    /// dot-product definition θ = acos((u·v)/(|u||v|)), u = r_i−r_j, v = r_k−r_j.
355    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    /// Center of mass, mass-weighted by standard atomic weights (falling back to
379    /// the atom's declared `mass`). Same length unit as the coordinates.
380    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    /// Principal moments of inertia (ascending), in amu·(length unit)². Builds the
408    /// exact inertia tensor about the center of mass and diagonalizes it with the
409    /// tested `scf::jacobi_diagonalization` (real symmetric 3×3).
410    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        // jacobi_diagonalization returns eigenvalues in ascending order.
453        Ok([evals[0], evals[1], evals[2]])
454    }
455
456    /// Aggregate the exact structural / mass properties into one result. The
457    /// nuclear repulsion energy is only meaningful when the coordinates are in
458    /// bohr; it is included here as `Some` when computable and `None` (with the
459    /// reason discarded) when the geometry cannot support it.
460    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
480/// Return the atom's 3-coordinate array, validating length. Shared by the exact
481/// structural-property methods above.
482fn 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}