use crate::error::{IntegrateError, IntegrateResult};
use crate::specialized::quantum::gaussian_integrals::GaussianBasis;
use crate::specialized::quantum::tdhf::scf::{
build_density_matrix, build_fock, electronic_energy, mat_mul, ScfResult,
};
use scirs2_core::ndarray::{Array2, ArrayView2};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Propagator {
Magnus2,
Euler,
}
#[derive(Debug, Clone)]
pub struct TdhfConfig {
pub dt: f64,
pub n_steps: usize,
pub propagator: Propagator,
pub field_strength: f64,
pub field_frequency: f64,
pub field_direction: [f64; 3],
}
impl Default for TdhfConfig {
fn default() -> Self {
Self {
dt: 0.1,
n_steps: 100,
propagator: Propagator::Magnus2,
field_strength: 0.0,
field_frequency: 0.05,
field_direction: [0.0, 0.0, 1.0],
}
}
}
#[derive(Debug, Clone)]
pub struct TdhfState {
pub density_matrix: Array2<f64>,
pub time: f64,
pub energy: f64,
}
#[derive(Debug, Clone)]
pub struct TdhfResult {
pub states: Vec<TdhfState>,
pub final_density: Array2<f64>,
pub dipole_moment_history: Vec<f64>,
}
fn build_dipole_z(basis: &[GaussianBasis], overlap: &Array2<f64>) -> Array2<f64> {
let n = basis.len();
let mut rz = Array2::zeros((n, n));
for mu in 0..n {
for nu in 0..n {
let centroid = 0.5 * (basis[mu].center[2] + basis[nu].center[2]);
rz[[mu, nu]] = centroid * overlap[[mu, nu]];
}
}
rz
}
fn dipole_z(p: &Array2<f64>, rz: &Array2<f64>) -> f64 {
let n = p.shape()[0];
let mut tr = 0.0;
for mu in 0..n {
for nu in 0..n {
tr += p[[mu, nu]] * rz[[nu, mu]];
}
}
-tr
}
pub fn matrix_exp_pade4(a: &Array2<f64>) -> Array2<f64> {
let n = a.shape()[0];
let b = [
1.0_f64,
0.5_f64,
0.12_f64, 1.833_333_333_333e-2, 1.992_753_623_188e-3, 8.333_333_333_333e-5, 1.388_888_888_889e-6, ];
let _ = b;
let norm_inf = inf_norm(a);
let s = if norm_inf > 0.5 {
(norm_inf / 0.5).log2().ceil() as u32
} else {
0
};
let scale = (1_u64 << s) as f64;
let a_scaled = a.mapv(|v| v / scale);
let a2 = mat_mul(&a_scaled, &a_scaled);
let a3 = mat_mul(&a2, &a_scaled);
let id = Array2::<f64>::eye(n);
let c0 = 120.0_f64;
let c1 = 60.0_f64;
let c2 = 12.0_f64;
let c3 = 1.0_f64;
let num =
id.mapv(|v| v * c0) + a_scaled.mapv(|v| v * c1) + a2.mapv(|v| v * c2) + a3.mapv(|v| v * c3);
let den =
id.mapv(|v| v * c0) - a_scaled.mapv(|v| v * c1) + a2.mapv(|v| v * c2) - a3.mapv(|v| v * c3);
let den_inv = mat_inv(&den);
let mut result = mat_mul(&num, &den_inv);
for _ in 0..s {
result = mat_mul(&result, &result);
}
result
}
fn inf_norm(a: &Array2<f64>) -> f64 {
let n = a.shape()[0];
(0..n)
.map(|i| (0..n).map(|j| a[[i, j]].abs()).sum::<f64>())
.fold(0.0_f64, f64::max)
}
fn mat_inv(a: &Array2<f64>) -> Array2<f64> {
let n = a.shape()[0];
let mut aug: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row: Vec<f64> = (0..n).map(|j| a[[i, j]]).collect();
let eye_row: Vec<f64> = (0..n).map(|j| if j == i { 1.0 } else { 0.0 }).collect();
row.extend(eye_row);
row
})
.collect();
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
if aug[row][col].abs() > max_val {
max_val = aug[row][col].abs();
max_row = row;
}
}
aug.swap(col, max_row);
let pivot = aug[col][col];
if pivot.abs() < 1e-14 {
return Array2::eye(n);
}
for row in (col + 1)..n {
let factor = aug[row][col] / pivot;
for k in 0..(2 * n) {
aug[row][k] -= factor * aug[col][k];
}
}
}
for col in (0..n).rev() {
let pivot = aug[col][col];
for k in 0..(2 * n) {
aug[col][k] /= pivot;
}
for row in 0..col {
let factor = aug[row][col];
for k in 0..(2 * n) {
aug[row][k] -= factor * aug[col][k];
}
}
}
let mut inv = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
inv[[i, j]] = aug[i][n + j];
}
}
inv
}
pub struct RealTimeTDHF {
config: TdhfConfig,
}
impl RealTimeTDHF {
pub fn new(config: TdhfConfig) -> Self {
Self { config }
}
pub fn propagate(
&self,
scf_result: &ScfResult,
basis: &[GaussianBasis],
nuclear_charges: &[(f64, [f64; 3])],
n_electrons: usize,
) -> IntegrateResult<TdhfResult> {
let n = basis.len();
let n_occ = n_electrons / 2;
let rz = build_dipole_z(basis, &scf_result.overlap);
let mut p = scf_result.density_matrix.clone();
let mut t = 0.0_f64;
let mut states = Vec::with_capacity(self.config.n_steps + 1);
let mut dipole_history = Vec::with_capacity(self.config.n_steps + 1);
let f0 = build_fock_with_field(
&p,
&scf_result.h_core,
&scf_result.eri,
n,
0.0,
&scf_result.overlap,
basis,
);
let e0 = electronic_energy(&p, &scf_result.h_core, &f0)
+ crate::specialized::quantum::tdhf::scf::nuclear_repulsion_energy(nuclear_charges);
states.push(TdhfState {
density_matrix: p.clone(),
time: t,
energy: e0,
});
dipole_history.push(dipole_z(&p, &rz));
for step in 0..self.config.n_steps {
let field_val = if self.config.field_strength.abs() < 1e-15 {
0.0
} else {
self.config.field_strength
* (self.config.field_frequency * t).cos()
* self.config.field_direction[2] };
let fock = build_fock_with_field(
&p,
&scf_result.h_core,
&scf_result.eri,
n,
field_val,
&scf_result.overlap,
basis,
);
p = match self.config.propagator {
Propagator::Magnus2 => {
let neg_f_dt = fock.mapv(|v| -v * self.config.dt);
let u = matrix_exp_pade4(&neg_f_dt);
let ut = u.t().to_owned();
let up = mat_mul(&u, &p);
mat_mul(&up, &ut)
}
Propagator::Euler => {
let fp = mat_mul(&fock, &p);
let pf = mat_mul(&p, &fock);
let commutator = fp - pf;
let p_new = p.clone() - commutator.mapv(|v| v * self.config.dt);
reproject_density(&p_new, &scf_result.overlap, n_occ, n)
}
};
t += self.config.dt;
let fock_new = build_fock_with_field(
&p,
&scf_result.h_core,
&scf_result.eri,
n,
0.0, &scf_result.overlap,
basis,
);
let e_elec = electronic_energy(&p, &scf_result.h_core, &fock_new);
let e_nuc =
crate::specialized::quantum::tdhf::scf::nuclear_repulsion_energy(nuclear_charges);
let energy = e_elec + e_nuc;
states.push(TdhfState {
density_matrix: p.clone(),
time: t,
energy,
});
dipole_history.push(dipole_z(&p, &rz));
let _ = step; }
let final_density = p;
Ok(TdhfResult {
states,
final_density,
dipole_moment_history: dipole_history,
})
}
}
fn reproject_density(p: &Array2<f64>, _s: &Array2<f64>, _n_occ: usize, n: usize) -> Array2<f64> {
let p2 = mat_mul(p, p);
let p3 = mat_mul(&p2, p);
let three_p2 = p2.mapv(|v| 3.0 * v);
let two_p3 = p3.mapv(|v| 2.0 * v);
let mut result = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
result[[i, j]] = three_p2[[i, j]] - two_p3[[i, j]];
}
}
result
}
pub fn build_fock_with_field(
p: &Array2<f64>,
h_core: &Array2<f64>,
eri: &[f64],
n_basis: usize,
field: f64,
overlap: &Array2<f64>,
basis: &[GaussianBasis],
) -> Array2<f64> {
let mut fock = build_fock(h_core, p, eri, n_basis);
if field.abs() > 1e-15 {
for mu in 0..n_basis {
for nu in 0..n_basis {
let centroid_z = 0.5 * (basis[mu].center[2] + basis[nu].center[2]);
fock[[mu, nu]] -= field * centroid_z * overlap[[mu, nu]];
}
}
}
fock
}
#[cfg(test)]
mod tests {
use super::*;
use crate::specialized::quantum::gaussian_integrals::normalized_s_gto;
use crate::specialized::quantum::tdhf::scf::{HartreeFockSCF, ScfConfig};
fn minimal_h2() -> (Vec<GaussianBasis>, Vec<(f64, [f64; 3])>) {
let basis = vec![
normalized_s_gto([0.0, 0.0, 0.0], 1.0),
normalized_s_gto([0.0, 0.0, 1.4], 1.0),
];
let charges = vec![
(1.0_f64, [0.0_f64, 0.0, 0.0]),
(1.0_f64, [0.0_f64, 0.0, 1.4]),
];
(basis, charges)
}
fn run_scf(basis: &[GaussianBasis], charges: &[(f64, [f64; 3])]) -> ScfResult {
HartreeFockSCF::new(ScfConfig::default())
.run(basis, charges, 2)
.unwrap()
}
#[test]
fn test_tdhf_config_default() {
let cfg = TdhfConfig::default();
assert_eq!(cfg.propagator, Propagator::Magnus2);
assert!(cfg.field_strength.abs() < 1e-15);
assert_eq!(cfg.field_direction, [0.0, 0.0, 1.0]);
}
#[test]
fn test_tdhf_zero_field_conserves_energy() {
let (basis, charges) = minimal_h2();
let scf = run_scf(&basis, &charges);
let cfg = TdhfConfig {
dt: 0.05,
n_steps: 20,
propagator: Propagator::Magnus2,
field_strength: 0.0,
..TdhfConfig::default()
};
let rt = RealTimeTDHF::new(cfg);
let result = rt.propagate(&scf, &basis, &charges, 2).unwrap();
let e0 = result.states[0].energy;
for state in &result.states {
let de = (state.energy - e0).abs();
assert!(de < 0.5, "Energy drift at t={}: ΔE={de}", state.time);
}
}
#[test]
fn test_tdhf_density_trace_preserved() {
let (basis, charges) = minimal_h2();
let scf = run_scf(&basis, &charges);
let cfg = TdhfConfig {
dt: 0.1,
n_steps: 10,
..TdhfConfig::default()
};
let result = RealTimeTDHF::new(cfg)
.propagate(&scf, &basis, &charges, 2)
.unwrap();
for state in &result.states {
let tr: f64 = (0..state.density_matrix.shape()[0])
.map(|i| state.density_matrix[[i, i]])
.sum();
assert!(
(tr - 2.0).abs() < 1.0,
"Density trace = {tr} at t={}",
state.time
);
}
}
#[test]
fn test_tdhf_propagate_completes() {
let (basis, charges) = minimal_h2();
let scf = run_scf(&basis, &charges);
let cfg = TdhfConfig {
dt: 0.1,
n_steps: 5,
propagator: Propagator::Magnus2,
..TdhfConfig::default()
};
let result = RealTimeTDHF::new(cfg)
.propagate(&scf, &basis, &charges, 2)
.unwrap();
assert_eq!(
result.states.len(),
6,
"Expected 6 states, got {}",
result.states.len()
);
assert_eq!(result.dipole_moment_history.len(), 6);
}
#[test]
fn test_tdhf_euler_propagate_completes() {
let (basis, charges) = minimal_h2();
let scf = run_scf(&basis, &charges);
let cfg = TdhfConfig {
dt: 0.01, n_steps: 5,
propagator: Propagator::Euler,
..TdhfConfig::default()
};
let result = RealTimeTDHF::new(cfg)
.propagate(&scf, &basis, &charges, 2)
.unwrap();
assert_eq!(result.states.len(), 6);
}
#[test]
fn test_matrix_exp_pade4_zero_matrix() {
let zero = Array2::zeros((3, 3));
let result = matrix_exp_pade4(&zero);
let id = Array2::<f64>::eye(3);
for i in 0..3 {
for j in 0..3 {
assert!(
(result[[i, j]] - id[[i, j]]).abs() < 1e-10,
"exp(0)[{i},{j}] = {}",
result[[i, j]]
);
}
}
}
#[test]
fn test_matrix_exp_pade4_diagonal() {
let mut a = Array2::zeros((2, 2));
a[[0, 0]] = 1.0;
a[[1, 1]] = -1.0;
let result = matrix_exp_pade4(&a);
assert!(
(result[[0, 0]] - 1.0_f64.exp()).abs() < 1e-4,
"exp(A)[0,0]={}, expected {}",
result[[0, 0]],
1.0_f64.exp()
);
assert!(
(result[[1, 1]] - (-1.0_f64).exp()).abs() < 1e-4,
"exp(A)[1,1]={}, expected {}",
result[[1, 1]],
(-1.0_f64).exp()
);
}
}