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#[derive(Debug, Clone, Copy)]
10struct ContractedS {
11 prims: [GtoPrimitive; 3],
13}
14
15fn build_sto3g_s(z: usize, center: [f64; 3]) -> Option<ContractedS> {
23 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 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 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
65fn 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
76fn 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
87fn 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
100fn 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
120fn 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
134fn run_rhf<const N: usize>(
138 shells: &[ContractedS; N],
139 nuclei: &[([f64; 3], f64)],
140 n_elec: usize,
141) -> Result<QuantumProperties, ChemistryError> {
142 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 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 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 (homo, 0.0)
197 };
198
199 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 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: 0.0,
234 mulliken_charges: mulliken,
235 })
236}
237
238pub struct QuantumCalculator {
240 wavefunction_calculator: WavefunctionCalculator,
241 energy_calculator: QuantumEnergyCalculator,
242 property_calculator: QuantumPropertyCalculator,
243}
244
245pub struct WavefunctionCalculator {
247 method_type: QuantumMethodType,
248 basis_set: BasisSet,
249 scf_parameters: SCFParameters,
250}
251
252#[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub enum BasisSetType {
276 Minimal,
277 SplitValence,
278 TripleZeta,
279 Polarization,
280 Diffuse,
281 Custom,
282}
283
284#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
296pub enum BasisFunctionType {
297 S,
298 P,
299 D,
300 F,
301 G,
302 Custom,
303}
304
305#[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
314pub struct QuantumEnergyCalculator {
316 electronic_energy: ElectronicEnergy,
317 nuclear_energy: NuclearEnergy,
318 total_energy: QuantumTotalEnergy,
319}
320
321#[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#[derive(Debug, Clone)]
332pub struct NuclearEnergy {
333 pub nuclear_repulsion: f64,
334 pub nuclear_attraction: f64,
335}
336
337#[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
346pub struct QuantumPropertyCalculator {
348 dipole_moment: DipoleMoment,
349 polarizability: Polarizability,
350 mulliken_charges: MullikenCharges,
351}
352
353#[derive(Debug, Clone)]
355pub struct DipoleMoment {
356 pub components: Vec<f64>,
357 pub magnitude: f64,
358}
359
360#[derive(Debug, Clone)]
362pub struct Polarizability {
363 pub tensor: Vec<Vec<f64>>,
364 pub isotropic: f64,
365}
366
367#[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 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 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 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 pub fn method_type(&self) -> &QuantumMethodType {
511 &self.method_type
512 }
513
514 pub fn set_method_type(&mut self, method_type: QuantumMethodType) {
516 self.method_type = method_type;
517 }
518
519 pub fn basis_set(&self) -> &BasisSet {
521 &self.basis_set
522 }
523
524 pub fn basis_set_mut(&mut self) -> &mut BasisSet {
526 &mut self.basis_set
527 }
528
529 pub fn scf_parameters(&self) -> &SCFParameters {
531 &self.scf_parameters
532 }
533
534 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 pub fn electronic_energy(&self) -> &ElectronicEnergy {
589 &self.electronic_energy
590 }
591
592 pub fn electronic_energy_mut(&mut self) -> &mut ElectronicEnergy {
594 &mut self.electronic_energy
595 }
596
597 pub fn nuclear_energy(&self) -> &NuclearEnergy {
599 &self.nuclear_energy
600 }
601
602 pub fn nuclear_energy_mut(&mut self) -> &mut NuclearEnergy {
604 &mut self.nuclear_energy
605 }
606
607 pub fn total_energy(&self) -> &QuantumTotalEnergy {
609 &self.total_energy
610 }
611
612 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 pub fn dipole_moment(&self) -> &DipoleMoment {
664 &self.dipole_moment
665 }
666
667 pub fn dipole_moment_mut(&mut self) -> &mut DipoleMoment {
669 &mut self.dipole_moment
670 }
671
672 pub fn polarizability(&self) -> &Polarizability {
674 &self.polarizability
675 }
676
677 pub fn polarizability_mut(&mut self) -> &mut Polarizability {
679 &mut self.polarizability
680 }
681
682 pub fn mulliken_charges(&self) -> &MullikenCharges {
684 &self.mulliken_charges
685 }
686
687 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}