Skip to main content

qualia_core_db/specialized_libs/chemistry_modeling/
quantum.rs

1use super::integrals::{GtoPrimitive, IntegralEngine};
2use super::scf::solve_rhf_scf_4index;
3use super::*;
4use crate::specialized_libs::shared::zero_heap_algebra::ZeroHeapMatrix;
5use core::f64::consts::PI;
6
7/// A contracted s-type Gaussian basis function (STO-3G): three primitives sharing
8/// a center, each carrying its fully-normalized effective coefficient.
9#[derive(Debug, Clone, Copy)]
10struct ContractedS {
11    /// The three STO-3G primitives; each carries the shell center in `origin`.
12    prims: [GtoPrimitive; 3],
13}
14
15/// Build the STO-3G 1s contracted basis function for a supported element
16/// (H, Z=1; He, Z=2). The three-Gaussian expansion of a Slater 1s uses the
17/// published STO-3G contraction coefficients (identical for the 1s shell of every
18/// element) with element-specific exponents. Each primitive coefficient is
19/// premultiplied by the primitive s normalization `(2α/π)^{3/4}`, and the whole
20/// contraction is renormalized so `<φ|φ> = 1`. Returns `None` for unsupported
21/// elements rather than inventing parameters.
22fn build_sto3g_s(z: usize, center: [f64; 3]) -> Option<ContractedS> {
23    // Published STO-3G data (Basis Set Exchange / EMSL).
24    let (exps, coefs): ([f64; 3], [f64; 3]) = match z {
25        1 => (
26            [3.425_250_914, 0.623_913_730, 0.168_855_400],
27            [0.154_328_967, 0.535_328_142, 0.444_634_542],
28        ),
29        2 => (
30            [6.362_421_394, 1.158_922_999, 0.313_649_790],
31            [0.154_328_967, 0.535_328_142, 0.444_634_542],
32        ),
33        _ => return None,
34    };
35    // Primitive normalization folded into the coefficient: d_i = c_i · (2α_i/π)^{3/4}.
36    let mut d = [0.0_f64; 3];
37    for i in 0..3 {
38        d[i] = coefs[i] * (2.0 * exps[i] / PI).powf(0.75);
39    }
40    // Contracted self-overlap  S = Σ_ij d_i d_j (π/(α_i+α_j))^{3/2}, then renormalize.
41    let mut s_cc = 0.0;
42    for i in 0..3 {
43        for j in 0..3 {
44            s_cc += d[i] * d[j] * (PI / (exps[i] + exps[j])).powf(1.5);
45        }
46    }
47    let renorm = 1.0 / s_cc.sqrt();
48    let mut prims = [GtoPrimitive {
49        origin: center,
50        exponent: 0.0,
51        l: [0, 0, 0],
52        coefficient: 0.0,
53    }; 3];
54    for i in 0..3 {
55        prims[i] = GtoPrimitive {
56            origin: center,
57            exponent: exps[i],
58            l: [0, 0, 0],
59            coefficient: d[i] * renorm,
60        };
61    }
62    Some(ContractedS { prims })
63}
64
65/// Contracted overlap `<a|b>` = Σ_ij `<g_i|g_j>` over the primitive pairs.
66fn c_overlap(a: &ContractedS, b: &ContractedS) -> f64 {
67    let mut v = 0.0;
68    for i in 0..3 {
69        for j in 0..3 {
70            v += IntegralEngine::overlap_s(&a.prims[i], &b.prims[j]);
71        }
72    }
73    v
74}
75
76/// Contracted kinetic energy `<a|−½∇²|b>`.
77fn c_kinetic(a: &ContractedS, b: &ContractedS) -> f64 {
78    let mut v = 0.0;
79    for i in 0..3 {
80        for j in 0..3 {
81            v += IntegralEngine::kinetic_s(&a.prims[i], &b.prims[j]);
82        }
83    }
84    v
85}
86
87/// Contracted nuclear attraction, summed over every nucleus `(center, Z)`.
88fn c_nuclear(a: &ContractedS, b: &ContractedS, nuclei: &[([f64; 3], f64)]) -> f64 {
89    let mut v = 0.0;
90    for &(center, z) in nuclei {
91        for i in 0..3 {
92            for j in 0..3 {
93                v += IntegralEngine::nuclear_s(&a.prims[i], &b.prims[j], center, z);
94            }
95        }
96    }
97    v
98}
99
100/// Contracted two-electron integral `(ab|cd)` in chemists' notation.
101fn c_eri(a: &ContractedS, b: &ContractedS, c: &ContractedS, d: &ContractedS) -> f64 {
102    let mut v = 0.0;
103    for i in 0..3 {
104        for j in 0..3 {
105            for k in 0..3 {
106                for l in 0..3 {
107                    v += IntegralEngine::evaluate_eri(
108                        &a.prims[i],
109                        &b.prims[j],
110                        &c.prims[k],
111                        &d.prims[l],
112                    );
113                }
114            }
115        }
116    }
117    v
118}
119
120/// Contracted Cartesian dipole integrals `[<a|x|b>, <a|y|b>, <a|z|b>]`.
121fn c_dipole(a: &ContractedS, b: &ContractedS) -> [f64; 3] {
122    let mut v = [0.0; 3];
123    for i in 0..3 {
124        for j in 0..3 {
125            let d = IntegralEngine::dipole_s(&a.prims[i], &b.prims[j]);
126            v[0] += d[0];
127            v[1] += d[1];
128            v[2] += d[2];
129        }
130    }
131    v
132}
133
134/// Run closed-shell RHF over `N` contracted s-type basis functions and assemble
135/// the electronic-structure observables. Real integrals, real 4-index Fock build,
136/// real diagonalization — no fabricated numbers.
137fn run_rhf<const N: usize>(
138    shells: &[ContractedS; N],
139    nuclei: &[([f64; 3], f64)],
140    n_elec: usize,
141) -> Result<QuantumProperties, ChemistryError> {
142    // Assemble overlap S, core Hamiltonian H = T + V_nuc, and the 4-index ERI.
143    let mut s = ZeroHeapMatrix::<f64, N, N>::zeros();
144    let mut h = ZeroHeapMatrix::<f64, N, N>::zeros();
145    for i in 0..N {
146        for j in 0..N {
147            s.set(i, j, c_overlap(&shells[i], &shells[j]));
148            h.set(
149                i,
150                j,
151                c_kinetic(&shells[i], &shells[j]) + c_nuclear(&shells[i], &shells[j], nuclei),
152            );
153        }
154    }
155    let mut eri = [[[[0.0_f64; N]; N]; N]; N];
156    for i in 0..N {
157        for j in 0..N {
158            for k in 0..N {
159                for l in 0..N {
160                    eri[i][j][k][l] = c_eri(&shells[i], &shells[j], &shells[k], &shells[l]);
161                }
162            }
163        }
164    }
165
166    let res = solve_rhf_scf_4index(&h, &s, &eri, n_elec).map_err(|e| {
167        ChemistryError::ConvergenceError(format!("RHF SCF did not converge: {:?}", e))
168    })?;
169
170    // Nuclear repulsion (coordinates assumed in bohr) → total energy.
171    let mut e_nn = 0.0;
172    for i in 0..nuclei.len() {
173        for j in (i + 1)..nuclei.len() {
174            let (ci, zi) = nuclei[i];
175            let (cj, zj) = nuclei[j];
176            let r = ((ci[0] - cj[0]).powi(2) + (ci[1] - cj[1]).powi(2) + (ci[2] - cj[2]).powi(2))
177                .sqrt();
178            e_nn += zi * zj / r;
179        }
180    }
181    let total_energy = res.electronic_energy + e_nn;
182
183    // HOMO / LUMO / gap from the orbital energies.
184    let num_occ = res.num_occ;
185    let homo = if num_occ >= 1 {
186        res.orbital_energies[num_occ - 1]
187    } else {
188        0.0
189    };
190    let (lumo, gap) = if N > num_occ {
191        let l = res.orbital_energies[num_occ];
192        (l, l - homo)
193    } else {
194        // Minimal basis with no virtual orbital (e.g. He/STO-3G): there is no
195        // LUMO. Report lumo = homo and gap = 0 to signal "no virtual in basis".
196        (homo, 0.0)
197    };
198
199    // Mulliken charges q_A = Z_A − (P·S)_AA (one basis function per atom here).
200    let ps = res.density * s;
201    let mut mulliken = Vec::with_capacity(N);
202    for i in 0..N {
203        mulliken.push(nuclei[i].1 - ps.get(i, i));
204    }
205
206    // Dipole μ = Σ_A Z_A R_A − Σ_μν P_μν <μ|r|ν>  (electron charge −1).
207    let mut dip = [0.0_f64; 3];
208    for &(center, z) in nuclei {
209        for w in 0..3 {
210            dip[w] += z * center[w];
211        }
212    }
213    for i in 0..N {
214        for j in 0..N {
215            let dij = c_dipole(&shells[i], &shells[j]);
216            let p = res.density.get(i, j);
217            for w in 0..3 {
218                dip[w] -= p * dij[w];
219            }
220        }
221    }
222    let dipole_magnitude = (dip[0] * dip[0] + dip[1] * dip[1] + dip[2] * dip[2]).sqrt();
223
224    Ok(QuantumProperties {
225        total_energy,
226        homo_energy: homo,
227        lumo_energy: lumo,
228        gap,
229        dipole_moment: dipole_magnitude,
230        // Polarizability is a response property (needs CPHF / finite-field
231        // perturbation) and is NOT one of the observables this RHF path computes;
232        // it is left at 0.0 and must not be read as a computed value.
233        polarizability: 0.0,
234        mulliken_charges: mulliken,
235    })
236}
237
238/// Quantum calculator for quantum chemistry calculations
239pub struct QuantumCalculator {
240    wavefunction_calculator: WavefunctionCalculator,
241    energy_calculator: QuantumEnergyCalculator,
242    property_calculator: QuantumPropertyCalculator,
243}
244
245/// Wavefunction calculator
246pub struct WavefunctionCalculator {
247    method_type: QuantumMethodType,
248    basis_set: BasisSet,
249    scf_parameters: SCFParameters,
250}
251
252/// Quantum method types
253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
254pub enum QuantumMethodType {
255    HartreeFock,
256    DFT,
257    MP2,
258    CCSD,
259    CI,
260    SemiEmpirical,
261    AbInitio,
262}
263
264/// Basis sets
265#[derive(Debug, Clone)]
266pub struct BasisSet {
267    pub basis_set_id: String,
268    pub basis_set_name: String,
269    pub basis_set_type: BasisSetType,
270    pub functions: Vec<BasisFunction>,
271}
272
273/// Basis set types
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub enum BasisSetType {
276    Minimal,
277    SplitValence,
278    TripleZeta,
279    Polarization,
280    Diffuse,
281    Custom,
282}
283
284/// Basis functions
285#[derive(Debug, Clone)]
286pub struct BasisFunction {
287    pub function_id: String,
288    pub function_type: BasisFunctionType,
289    pub center: Vec<f64>,
290    pub exponents: Vec<f64>,
291    pub coefficients: Vec<f64>,
292}
293
294/// Basis function types
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
296pub enum BasisFunctionType {
297    S,
298    P,
299    D,
300    F,
301    G,
302    Custom,
303}
304
305/// SCF parameters
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct SCFParameters {
308    pub convergence_threshold: f64,
309    pub max_iterations: u32,
310    pub damping_factor: f64,
311    pub level_shifting: f64,
312}
313
314/// Quantum energy calculator
315pub struct QuantumEnergyCalculator {
316    electronic_energy: ElectronicEnergy,
317    nuclear_energy: NuclearEnergy,
318    total_energy: QuantumTotalEnergy,
319}
320
321/// Electronic energy
322#[derive(Debug, Clone)]
323pub struct ElectronicEnergy {
324    pub kinetic_energy: f64,
325    pub electron_nuclear: f64,
326    pub electron_electron: f64,
327    pub exchange_correlation: f64,
328}
329
330/// Nuclear energy
331#[derive(Debug, Clone)]
332pub struct NuclearEnergy {
333    pub nuclear_repulsion: f64,
334    pub nuclear_attraction: f64,
335}
336
337/// Quantum total energy
338#[derive(Debug, Clone)]
339pub struct QuantumTotalEnergy {
340    pub electronic: f64,
341    pub nuclear: f64,
342    pub total: f64,
343    pub correction_terms: Vec<f64>,
344}
345
346/// Quantum property calculator
347pub struct QuantumPropertyCalculator {
348    dipole_moment: DipoleMoment,
349    polarizability: Polarizability,
350    mulliken_charges: MullikenCharges,
351}
352
353/// Dipole moment
354#[derive(Debug, Clone)]
355pub struct DipoleMoment {
356    pub components: Vec<f64>,
357    pub magnitude: f64,
358}
359
360/// Polarizability
361#[derive(Debug, Clone)]
362pub struct Polarizability {
363    pub tensor: Vec<Vec<f64>>,
364    pub isotropic: f64,
365}
366
367/// Mulliken charges
368#[derive(Debug, Clone)]
369pub struct MullikenCharges {
370    pub charges: Vec<f64>,
371    pub total_charge: f64,
372}
373
374impl QuantumCalculator {
375    pub fn new() -> Self {
376        Self {
377            wavefunction_calculator: WavefunctionCalculator::new(),
378            energy_calculator: QuantumEnergyCalculator::new(),
379            property_calculator: QuantumPropertyCalculator::new(),
380        }
381    }
382
383    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
384        self.wavefunction_calculator.initialize()?;
385        self.energy_calculator.initialize()?;
386        self.property_calculator.initialize()?;
387        Ok(())
388    }
389
390    pub fn validate_molecule(&self, molecule: &Molecule) -> Result<(), ChemistryError> {
391        if molecule.atoms.is_empty() {
392            return Err(ChemistryError::ValidationError(
393                "Molecule must have at least one atom".to_string(),
394            ));
395        }
396        Ok(())
397    }
398
399    /// Compute the ground-state electronic-structure observables by running a real
400    /// RHF/STO-3G calculation: total SCF energy, HOMO/LUMO orbital energies and
401    /// gap, Mulliken charges, and the dipole moment.
402    ///
403    /// The energy falls out of a genuine self-consistent field over analytical
404    /// one- and two-electron integrals (kinetic, nuclear attraction, overlap,
405    /// four-index ERI) — nothing is hardcoded. Atomic `coordinates` are taken to
406    /// be in **bohr** (atomic units); the returned energies are in Hartree.
407    ///
408    /// Scope of this deterministic path: closed-shell (even electron count)
409    /// molecules built from H and He, for which STO-3G is a single s-type shell
410    /// per atom with exact closed-form s integrals. Anything outside that scope
411    /// (heavier elements needing p/d shells, open-shell/odd-electron systems, or a
412    /// requested method other than Hartree-Fock) returns an honest `NotImplemented`
413    /// rather than a fabricated number.
414    pub fn calculate_properties(
415        &mut self,
416        molecule: &Molecule,
417        method: QuantumMethodType,
418    ) -> Result<QuantumProperties, ChemistryError> {
419        match method {
420            QuantumMethodType::HartreeFock | QuantumMethodType::AbInitio => {}
421            other => {
422                return Err(ChemistryError::NotImplemented(format!(
423                    "electronic structure: only closed-shell RHF (HartreeFock) is implemented; \
424                     {:?} would require a correlated/DFT method that is not yet built",
425                    other
426                )));
427            }
428        }
429
430        if molecule.atoms.is_empty() {
431            return Err(ChemistryError::ValidationError(
432                "electronic structure: the molecule has no atoms".to_string(),
433            ));
434        }
435
436        // Marshal the molecule into an STO-3G contracted basis (one s shell per
437        // H/He atom) and collect the nuclei. Refuse unsupported elements.
438        let mut shells: Vec<ContractedS> = Vec::with_capacity(molecule.atoms.len());
439        let mut nuclei: Vec<([f64; 3], f64)> = Vec::with_capacity(molecule.atoms.len());
440        let mut n_elec = 0usize;
441        for (idx, a) in molecule.atoms.iter().enumerate() {
442            if a.coordinates.len() != 3 {
443                return Err(ChemistryError::InsufficientData(format!(
444                    "electronic structure: atom {} ('{}') has {} coordinates; 3 (in bohr) required",
445                    idx,
446                    a.atom_id,
447                    a.coordinates.len()
448                )));
449            }
450            let center = [a.coordinates[0], a.coordinates[1], a.coordinates[2]];
451            let shell = build_sto3g_s(a.atomic_number, center).ok_or_else(|| {
452                ChemistryError::NotImplemented(format!(
453                    "electronic structure: STO-3G RHF is implemented for H and He only; atom {} \
454                     ('{}', element '{}', Z={}) needs p/d shells that are not yet built",
455                    idx, a.atom_id, a.element, a.atomic_number
456                ))
457            })?;
458            shells.push(shell);
459            nuclei.push((center, a.atomic_number as f64));
460            n_elec += a.atomic_number;
461        }
462
463        if n_elec == 0 {
464            return Err(ChemistryError::ValidationError(
465                "electronic structure: total electron count is zero".to_string(),
466            ));
467        }
468        if n_elec % 2 != 0 {
469            return Err(ChemistryError::NotImplemented(format!(
470                "electronic structure: {} electrons is open-shell; only closed-shell RHF (even \
471                 electron count) is implemented — UHF is not yet built",
472                n_elec
473            )));
474        }
475
476        let n_bf = shells.len();
477        // Dispatch to the const-generic RHF driver for the supported basis sizes.
478        macro_rules! dispatch {
479            ($($n:literal),+ $(,)?) => {
480                match n_bf {
481                    $(
482                        $n => {
483                            let mut arr = [shells[0]; $n];
484                            for i in 0..$n { arr[i] = shells[i]; }
485                            run_rhf::<$n>(&arr, &nuclei, n_elec)
486                        }
487                    )+
488                    _ => Err(ChemistryError::NotImplemented(format!(
489                        "electronic structure: STO-3G RHF currently supports 1–8 s-type basis \
490                         functions (H/He atoms); this molecule needs {}",
491                        n_bf
492                    ))),
493                }
494            };
495        }
496        dispatch!(1, 2, 3, 4, 5, 6, 7, 8)
497    }
498}
499
500impl WavefunctionCalculator {
501    pub fn new() -> Self {
502        Self {
503            method_type: QuantumMethodType::HartreeFock,
504            basis_set: BasisSet::new(),
505            scf_parameters: SCFParameters::new(),
506        }
507    }
508
509    /// Borrow the quantum method type.
510    pub fn method_type(&self) -> &QuantumMethodType {
511        &self.method_type
512    }
513
514    /// Set the quantum method type.
515    pub fn set_method_type(&mut self, method_type: QuantumMethodType) {
516        self.method_type = method_type;
517    }
518
519    /// Borrow the basis set.
520    pub fn basis_set(&self) -> &BasisSet {
521        &self.basis_set
522    }
523
524    /// Mutably borrow the basis set.
525    pub fn basis_set_mut(&mut self) -> &mut BasisSet {
526        &mut self.basis_set
527    }
528
529    /// Borrow the SCF parameters.
530    pub fn scf_parameters(&self) -> &SCFParameters {
531        &self.scf_parameters
532    }
533
534    /// Mutably borrow the SCF parameters.
535    pub fn scf_parameters_mut(&mut self) -> &mut SCFParameters {
536        &mut self.scf_parameters
537    }
538
539    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
540        Ok(())
541    }
542}
543
544impl BasisSet {
545    pub fn new() -> Self {
546        Self {
547            basis_set_id: "basis_1".to_string(),
548            basis_set_name: "6-31G".to_string(),
549            basis_set_type: BasisSetType::SplitValence,
550            functions: vec![BasisFunction::new()],
551        }
552    }
553}
554
555impl BasisFunction {
556    pub fn new() -> Self {
557        Self {
558            function_id: "func_1".to_string(),
559            function_type: BasisFunctionType::S,
560            center: vec![0.0, 0.0, 0.0],
561            exponents: vec![0.5],
562            coefficients: vec![1.0],
563        }
564    }
565}
566
567impl SCFParameters {
568    pub fn new() -> Self {
569        Self {
570            convergence_threshold: 1e-8,
571            max_iterations: 100,
572            damping_factor: 0.5,
573            level_shifting: 0.3,
574        }
575    }
576}
577
578impl QuantumEnergyCalculator {
579    pub fn new() -> Self {
580        Self {
581            electronic_energy: ElectronicEnergy::new(),
582            nuclear_energy: NuclearEnergy::new(),
583            total_energy: QuantumTotalEnergy::new(),
584        }
585    }
586
587    /// Borrow the electronic-energy breakdown.
588    pub fn electronic_energy(&self) -> &ElectronicEnergy {
589        &self.electronic_energy
590    }
591
592    /// Mutably borrow the electronic-energy breakdown.
593    pub fn electronic_energy_mut(&mut self) -> &mut ElectronicEnergy {
594        &mut self.electronic_energy
595    }
596
597    /// Borrow the nuclear-energy breakdown.
598    pub fn nuclear_energy(&self) -> &NuclearEnergy {
599        &self.nuclear_energy
600    }
601
602    /// Mutably borrow the nuclear-energy breakdown.
603    pub fn nuclear_energy_mut(&mut self) -> &mut NuclearEnergy {
604        &mut self.nuclear_energy
605    }
606
607    /// Borrow the total-energy breakdown.
608    pub fn total_energy(&self) -> &QuantumTotalEnergy {
609        &self.total_energy
610    }
611
612    /// Mutably borrow the total-energy breakdown.
613    pub fn total_energy_mut(&mut self) -> &mut QuantumTotalEnergy {
614        &mut self.total_energy
615    }
616
617    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
618        Ok(())
619    }
620}
621
622impl ElectronicEnergy {
623    pub fn new() -> Self {
624        Self {
625            kinetic_energy: 0.0,
626            electron_nuclear: 0.0,
627            electron_electron: 0.0,
628            exchange_correlation: 0.0,
629        }
630    }
631}
632
633impl NuclearEnergy {
634    pub fn new() -> Self {
635        Self {
636            nuclear_repulsion: 0.0,
637            nuclear_attraction: 0.0,
638        }
639    }
640}
641
642impl QuantumTotalEnergy {
643    pub fn new() -> Self {
644        Self {
645            electronic: 0.0,
646            nuclear: 0.0,
647            total: 0.0,
648            correction_terms: Vec::new(),
649        }
650    }
651}
652
653impl QuantumPropertyCalculator {
654    pub fn new() -> Self {
655        Self {
656            dipole_moment: DipoleMoment::new(),
657            polarizability: Polarizability::new(),
658            mulliken_charges: MullikenCharges::new(),
659        }
660    }
661
662    /// Borrow the dipole-moment result.
663    pub fn dipole_moment(&self) -> &DipoleMoment {
664        &self.dipole_moment
665    }
666
667    /// Mutably borrow the dipole-moment result.
668    pub fn dipole_moment_mut(&mut self) -> &mut DipoleMoment {
669        &mut self.dipole_moment
670    }
671
672    /// Borrow the polarizability result.
673    pub fn polarizability(&self) -> &Polarizability {
674        &self.polarizability
675    }
676
677    /// Mutably borrow the polarizability result.
678    pub fn polarizability_mut(&mut self) -> &mut Polarizability {
679        &mut self.polarizability
680    }
681
682    /// Borrow the Mulliken-charges result.
683    pub fn mulliken_charges(&self) -> &MullikenCharges {
684        &self.mulliken_charges
685    }
686
687    /// Mutably borrow the Mulliken-charges result.
688    pub fn mulliken_charges_mut(&mut self) -> &mut MullikenCharges {
689        &mut self.mulliken_charges
690    }
691
692    pub fn initialize(&mut self) -> Result<(), ChemistryError> {
693        Ok(())
694    }
695}
696
697impl DipoleMoment {
698    pub fn new() -> Self {
699        Self {
700            components: vec![0.0, 0.0, 0.0],
701            magnitude: 0.0,
702        }
703    }
704}
705
706impl Polarizability {
707    pub fn new() -> Self {
708        Self {
709            tensor: vec![vec![0.0; 3]; 3],
710            isotropic: 0.0,
711        }
712    }
713}
714
715impl MullikenCharges {
716    pub fn new() -> Self {
717        Self {
718            charges: Vec::new(),
719            total_charge: 0.0,
720        }
721    }
722}